@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/node.js
ADDED
|
@@ -0,0 +1,1414 @@
|
|
|
1
|
+
import { A as candidateLabel, I as findInferenceConfigIssues, a as inferenceProviderBindingFor, l as parseInferenceProviderConfig, o as inferenceProviderConfigSecretName, t as INFERENCE_PROVIDER_CONFIG_SECRET_TYPE, w as classifyInferenceFailure } from "./chunks/provider-config-BSpIQRVM.js";
|
|
2
|
+
import { invalidateUserInferenceResolvers } from "./resolver.js";
|
|
3
|
+
import { INFERENCE_PRESETS, INFERENCE_PRESET_CAPABILITIES, INFERENCE_TIERS, assertInferenceConfig, ensureSystemTables, getInferenceConfig, inferenceCapabilitiesFromSdk, inferenceCapabilityHeuristic, inferencePresetFor, inferenceTargetKind, isInferencePresetId, isPostgresDatabase, resolveInferenceCapabilities, runOnce } from "@happyvertical/smrt-core";
|
|
4
|
+
import { SmrtError } from "@happyvertical/smrt-core/smrt-error";
|
|
5
|
+
import { AI_PROVIDER_TYPES } from "@happyvertical/ai";
|
|
6
|
+
import { getDefaultAIKeyEnvName, redactBaseUrl } from "@happyvertical/smrt-config";
|
|
7
|
+
import { SecretService } from "@happyvertical/smrt-secrets";
|
|
8
|
+
import { withTenant } from "@happyvertical/smrt-tenancy";
|
|
9
|
+
//#region src/hosted.ts
|
|
10
|
+
var HOSTED_INFERENCE_FEATURE_KEY = "inference.hosted";
|
|
11
|
+
var HOSTED_INFERENCE_SECRET_CATEGORY = "inference.hosted";
|
|
12
|
+
var HOSTED_GATEWAY_PROVIDERS = ["bifrost", "litellm"];
|
|
13
|
+
var HOSTED_BUDGET_WINDOWS = [
|
|
14
|
+
"day",
|
|
15
|
+
"week",
|
|
16
|
+
"month",
|
|
17
|
+
"year"
|
|
18
|
+
];
|
|
19
|
+
var WINDOW_DURATION = {
|
|
20
|
+
day: "1d",
|
|
21
|
+
week: "7d",
|
|
22
|
+
month: "30d",
|
|
23
|
+
year: "365d"
|
|
24
|
+
};
|
|
25
|
+
var HostedInferenceError = class extends SmrtError {
|
|
26
|
+
constructor(message, code, details = {}) {
|
|
27
|
+
super(message, code, "configuration", details);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var HOSTED_GATEWAY_ADMIN_TIMEOUT_MS = 3e4;
|
|
31
|
+
var HOSTED_GATEWAY_ADMIN_METHODS = [
|
|
32
|
+
"createProject",
|
|
33
|
+
"createVirtualKey",
|
|
34
|
+
"updateVirtualKey",
|
|
35
|
+
"deleteVirtualKey",
|
|
36
|
+
"listProjects"
|
|
37
|
+
];
|
|
38
|
+
var HOSTED_GATEWAY_ENV = {
|
|
39
|
+
provider: "SMRT_INFERENCE_HOSTED_PROVIDER",
|
|
40
|
+
baseUrl: "SMRT_INFERENCE_HOSTED_BASE_URL",
|
|
41
|
+
adminUrl: "SMRT_INFERENCE_HOSTED_ADMIN_URL",
|
|
42
|
+
adminApiKey: "SMRT_INFERENCE_HOSTED_ADMIN_KEY",
|
|
43
|
+
adminUser: "SMRT_INFERENCE_HOSTED_ADMIN_USER",
|
|
44
|
+
adminPassword: "SMRT_INFERENCE_HOSTED_ADMIN_PASSWORD",
|
|
45
|
+
model: "SMRT_INFERENCE_HOSTED_MODEL",
|
|
46
|
+
models: "SMRT_INFERENCE_HOSTED_MODELS",
|
|
47
|
+
keyDuration: "SMRT_INFERENCE_HOSTED_KEY_DURATION",
|
|
48
|
+
adminTimeoutMs: "SMRT_INFERENCE_HOSTED_ADMIN_TIMEOUT_MS"
|
|
49
|
+
};
|
|
50
|
+
function text$1(value) {
|
|
51
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
52
|
+
}
|
|
53
|
+
function isPlainHttpUrl(value) {
|
|
54
|
+
try {
|
|
55
|
+
const url = new URL(value);
|
|
56
|
+
return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password && !url.search && !url.hash;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function parseHostedGatewayConfig(value) {
|
|
62
|
+
const issues = [];
|
|
63
|
+
const provider = text$1(value.provider)?.toLowerCase();
|
|
64
|
+
if (!provider || !HOSTED_GATEWAY_PROVIDERS.includes(provider)) issues.push(`provider must be one of ${HOSTED_GATEWAY_PROVIDERS.join(", ")}`);
|
|
65
|
+
const baseUrl = text$1(value.baseUrl);
|
|
66
|
+
if (!baseUrl || !isPlainHttpUrl(baseUrl)) issues.push("baseUrl must be an http(s) URL without credentials, query or fragment");
|
|
67
|
+
const adminUrl = text$1(value.adminUrl);
|
|
68
|
+
if (adminUrl && !isPlainHttpUrl(adminUrl)) issues.push("adminUrl must be an http(s) URL without credentials, query or fragment");
|
|
69
|
+
const adminApiKey = text$1(value.adminApiKey);
|
|
70
|
+
const adminUser = text$1(value.adminUser);
|
|
71
|
+
const adminPassword = text$1(value.adminPassword);
|
|
72
|
+
if (provider === "litellm" && !adminApiKey) issues.push("adminApiKey (the LiteLLM master key) is required for litellm");
|
|
73
|
+
if (provider === "bifrost" && !adminApiKey && !(adminUser && adminPassword)) issues.push("bifrost needs adminApiKey or both adminUser and adminPassword");
|
|
74
|
+
const tiers = {};
|
|
75
|
+
for (const tier of INFERENCE_TIERS) {
|
|
76
|
+
const model2 = text$1(value.tiers?.[tier]);
|
|
77
|
+
if (model2) tiers[tier] = model2;
|
|
78
|
+
}
|
|
79
|
+
const adminTimeoutMs = value.adminTimeoutMs;
|
|
80
|
+
if (adminTimeoutMs !== void 0 && (!Number.isSafeInteger(adminTimeoutMs) || adminTimeoutMs <= 0)) issues.push("adminTimeoutMs must be a positive integer (ms)");
|
|
81
|
+
const model = text$1(value.model);
|
|
82
|
+
const models = (value.models ?? []).map((entry) => text$1(entry)).filter((entry) => !!entry);
|
|
83
|
+
if (issues.length > 0) throw new HostedInferenceError(`Hosted inference gateway is not configured: ${issues.join("; ")}`, "HOSTED_GATEWAY_NOT_CONFIGURED", { issues });
|
|
84
|
+
return {
|
|
85
|
+
provider,
|
|
86
|
+
baseUrl,
|
|
87
|
+
...adminUrl ? { adminUrl } : {},
|
|
88
|
+
...adminApiKey ? { adminApiKey } : {},
|
|
89
|
+
...adminUser ? { adminUser } : {},
|
|
90
|
+
...adminPassword ? { adminPassword } : {},
|
|
91
|
+
...model ? { model } : {},
|
|
92
|
+
...Object.keys(tiers).length > 0 ? { tiers } : {},
|
|
93
|
+
...models.length > 0 ? { models } : {},
|
|
94
|
+
...text$1(value.keyDuration) ? { keyDuration: text$1(value.keyDuration) } : {},
|
|
95
|
+
...adminTimeoutMs !== void 0 ? { adminTimeoutMs } : {}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function hostedGatewayConfigFromEnv(env = process.env) {
|
|
99
|
+
if (!text$1(env[HOSTED_GATEWAY_ENV.provider])) return void 0;
|
|
100
|
+
const tiers = {};
|
|
101
|
+
for (const tier of INFERENCE_TIERS) {
|
|
102
|
+
const model = text$1(env[`SMRT_INFERENCE_HOSTED_${tier.toUpperCase()}_MODEL`]);
|
|
103
|
+
if (model) tiers[tier] = model;
|
|
104
|
+
}
|
|
105
|
+
return parseHostedGatewayConfig({
|
|
106
|
+
provider: env[HOSTED_GATEWAY_ENV.provider],
|
|
107
|
+
baseUrl: env[HOSTED_GATEWAY_ENV.baseUrl],
|
|
108
|
+
adminUrl: env[HOSTED_GATEWAY_ENV.adminUrl],
|
|
109
|
+
adminApiKey: env[HOSTED_GATEWAY_ENV.adminApiKey],
|
|
110
|
+
adminUser: env[HOSTED_GATEWAY_ENV.adminUser],
|
|
111
|
+
adminPassword: env[HOSTED_GATEWAY_ENV.adminPassword],
|
|
112
|
+
model: env[HOSTED_GATEWAY_ENV.model],
|
|
113
|
+
tiers,
|
|
114
|
+
models: (env[HOSTED_GATEWAY_ENV.models] ?? "").split(","),
|
|
115
|
+
keyDuration: env[HOSTED_GATEWAY_ENV.keyDuration],
|
|
116
|
+
...text$1(env[HOSTED_GATEWAY_ENV.adminTimeoutMs]) ? { adminTimeoutMs: Number(text$1(env[HOSTED_GATEWAY_ENV.adminTimeoutMs])) } : {}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
var envConfig;
|
|
120
|
+
function getHostedGatewayConfig() {
|
|
121
|
+
envConfig ??= { value: hostedGatewayConfigFromEnv() };
|
|
122
|
+
if (!envConfig.value) throw new HostedInferenceError(`Hosted inference gateway is not configured: set ${HOSTED_GATEWAY_ENV.provider} (bifrost or litellm), ${HOSTED_GATEWAY_ENV.baseUrl} and the admin credentials.`, "HOSTED_GATEWAY_NOT_CONFIGURED");
|
|
123
|
+
return envConfig.value;
|
|
124
|
+
}
|
|
125
|
+
function resetHostedGatewayConfig() {
|
|
126
|
+
envConfig = void 0;
|
|
127
|
+
}
|
|
128
|
+
async function createHostedGatewayAdmin(config) {
|
|
129
|
+
const { getAI } = await import("@happyvertical/ai");
|
|
130
|
+
const common = {
|
|
131
|
+
baseUrl: config.baseUrl,
|
|
132
|
+
apiKey: config.adminApiKey ?? "smrt-admin",
|
|
133
|
+
timeout: config.adminTimeoutMs ?? 3e4,
|
|
134
|
+
...config.adminUrl ? { adminUrl: config.adminUrl } : {},
|
|
135
|
+
...config.adminApiKey ? { adminApiKey: config.adminApiKey } : {}
|
|
136
|
+
};
|
|
137
|
+
const admin = (config.provider === "bifrost" ? await getAI({
|
|
138
|
+
type: "bifrost",
|
|
139
|
+
...common,
|
|
140
|
+
...config.adminUser ? { adminUser: config.adminUser } : {},
|
|
141
|
+
...config.adminPassword ? { adminPassword: config.adminPassword } : {}
|
|
142
|
+
}) : await getAI({
|
|
143
|
+
type: "litellm",
|
|
144
|
+
...common
|
|
145
|
+
})).admin;
|
|
146
|
+
const missing = HOSTED_GATEWAY_ADMIN_METHODS.filter((method) => typeof admin?.[method] !== "function");
|
|
147
|
+
if (!admin || missing.length > 0) throw new HostedInferenceError(`The ${config.provider} provider of @happyvertical/ai lacks admin calls: ${missing.join(", ")} (needs @happyvertical/ai >= 0.111.7).`, "HOSTED_GATEWAY_UNSUPPORTED", {
|
|
148
|
+
provider: config.provider,
|
|
149
|
+
missing
|
|
150
|
+
});
|
|
151
|
+
return admin;
|
|
152
|
+
}
|
|
153
|
+
var HOSTED_BUDGET_UNCERTAIN_WINDOW_MS = 15 * 6e4;
|
|
154
|
+
var STATE_PREFIX = `${HOSTED_INFERENCE_SECRET_CATEGORY}:user:`;
|
|
155
|
+
var PROJECT_NAME = `${HOSTED_INFERENCE_SECRET_CATEGORY}:project`;
|
|
156
|
+
var RUN_ONCE_ACTOR_PROJECT = "smrt-inference-hosted";
|
|
157
|
+
function hostedProjectName(tenantId) {
|
|
158
|
+
return `smrt tenant ${tenantId}`;
|
|
159
|
+
}
|
|
160
|
+
function hostedInferenceStateSecretName(userId) {
|
|
161
|
+
return `${STATE_PREFIX}${userId}`;
|
|
162
|
+
}
|
|
163
|
+
var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@-]{0,199}$/;
|
|
164
|
+
function requireId(value, field) {
|
|
165
|
+
const id = text$1(value);
|
|
166
|
+
if (!id || !ID_PATTERN.test(id)) throw new HostedInferenceError(`${field} must be 1-200 characters of letters, digits and . _ : @ -`, "HOSTED_INFERENCE_INVALID", { field });
|
|
167
|
+
return id;
|
|
168
|
+
}
|
|
169
|
+
function normalizeHostedBudget(budget) {
|
|
170
|
+
const limit = budget?.limitMicros;
|
|
171
|
+
if (typeof limit !== "number" || !Number.isSafeInteger(limit) || limit <= 0) throw new HostedInferenceError("budget.limitMicros must be a positive integer (micro-dollars)", "HOSTED_INFERENCE_INVALID", { field: "budget.limitMicros" });
|
|
172
|
+
const window = budget?.window ?? "month";
|
|
173
|
+
if (!HOSTED_BUDGET_WINDOWS.includes(window)) throw new HostedInferenceError(`budget.window must be one of ${HOSTED_BUDGET_WINDOWS.join(", ")}`, "HOSTED_INFERENCE_INVALID", { field: "budget.window" });
|
|
174
|
+
return {
|
|
175
|
+
limitMicros: limit,
|
|
176
|
+
window
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function toGatewayBudget(budget) {
|
|
180
|
+
return {
|
|
181
|
+
maxLimit: budget.limitMicros / 1e6,
|
|
182
|
+
resetDuration: WINDOW_DURATION[budget.window]
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function sameBudget(a, b) {
|
|
186
|
+
return !!a && a.limitMicros === b.limitMicros && a.window === b.window;
|
|
187
|
+
}
|
|
188
|
+
var KEY_VALUE_PATTERN = /\bsk-[A-Za-z0-9_-]{8,}/;
|
|
189
|
+
function safeKeyId(id, apiKey) {
|
|
190
|
+
const handle = text$1(id);
|
|
191
|
+
if (!handle || handle === apiKey || KEY_VALUE_PATTERN.test(handle)) return null;
|
|
192
|
+
return handle;
|
|
193
|
+
}
|
|
194
|
+
function parseState(value) {
|
|
195
|
+
let raw;
|
|
196
|
+
try {
|
|
197
|
+
raw = JSON.parse(value);
|
|
198
|
+
} catch {
|
|
199
|
+
throw new HostedInferenceError("Stored hosted inference state is not JSON", "HOSTED_STATE_INVALID");
|
|
200
|
+
}
|
|
201
|
+
const state = raw;
|
|
202
|
+
if (!state || typeof state !== "object" || state.version !== 1 || state.status !== "active" && state.status !== "revoked" || typeof state.generation !== "number" || !Number.isSafeInteger(state.generation) || typeof state.keyAlias !== "string" || state.status === "active" && (typeof state.projectId !== "string" || !state.budget)) throw new HostedInferenceError("Stored hosted inference state is invalid", "HOSTED_STATE_INVALID");
|
|
203
|
+
return {
|
|
204
|
+
...state,
|
|
205
|
+
keyId: safeKeyId(state.keyId),
|
|
206
|
+
...Array.isArray(state.pendingGatewayRevocations) ? { pendingGatewayRevocations: state.pendingGatewayRevocations.map((entry) => ({
|
|
207
|
+
...entry,
|
|
208
|
+
keyId: safeKeyId(entry.keyId)
|
|
209
|
+
})) } : {},
|
|
210
|
+
prior: state.prior ? parseInferenceProviderConfig(state.prior) : null
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function isAbsentSecretError(error, name) {
|
|
214
|
+
if (!(error instanceof Error)) return false;
|
|
215
|
+
return error.message === `Secret '${name}' not found` || error.message === "Secret not found" || error.message === "Secret disabled" || error.message === "Secret expired";
|
|
216
|
+
}
|
|
217
|
+
function describeState(tenantId, userId, state) {
|
|
218
|
+
if (!state) return {
|
|
219
|
+
tenantId,
|
|
220
|
+
userId,
|
|
221
|
+
status: "none",
|
|
222
|
+
generation: 0
|
|
223
|
+
};
|
|
224
|
+
return {
|
|
225
|
+
tenantId,
|
|
226
|
+
userId,
|
|
227
|
+
status: state.status,
|
|
228
|
+
generation: state.generation,
|
|
229
|
+
...state.provider ? { provider: state.provider } : {},
|
|
230
|
+
...state.projectId ? { projectId: state.projectId } : {},
|
|
231
|
+
keyId: state.keyId,
|
|
232
|
+
...state.keyAlias ? { keyAlias: state.keyAlias } : {},
|
|
233
|
+
...state.budget ? { budget: state.budget } : {},
|
|
234
|
+
...state.pendingGatewayRevocations?.length ? { pendingGatewayRevocations: state.pendingGatewayRevocations } : {}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
var GATEWAY_TIMEOUT_CODE = "HOSTED_GATEWAY_TIMEOUT";
|
|
238
|
+
async function withDeadline(call, ms, operation) {
|
|
239
|
+
let timer;
|
|
240
|
+
const deadline = new Promise((_, reject) => {
|
|
241
|
+
timer = setTimeout(() => reject(Object.assign(/* @__PURE__ */ new Error(`Gateway ${operation} timed out`), { code: GATEWAY_TIMEOUT_CODE })), ms);
|
|
242
|
+
});
|
|
243
|
+
try {
|
|
244
|
+
return await Promise.race([call(), deadline]);
|
|
245
|
+
} finally {
|
|
246
|
+
if (timer) clearTimeout(timer);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function isGatewayTimeout(error) {
|
|
250
|
+
return !!error && typeof error === "object" && error.code === GATEWAY_TIMEOUT_CODE;
|
|
251
|
+
}
|
|
252
|
+
function mayHaveCreated(error) {
|
|
253
|
+
if (isGatewayTimeout(error)) return true;
|
|
254
|
+
const failure = classifyInferenceFailure(error);
|
|
255
|
+
return failure !== "credential" && failure !== "quota" && failure !== "missing-model" && failure !== "refusal";
|
|
256
|
+
}
|
|
257
|
+
function gatewayFailure(operation, error) {
|
|
258
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
|
|
259
|
+
const name = error instanceof Error ? error.name : typeof error;
|
|
260
|
+
return new HostedInferenceError(`Gateway ${operation} failed (${code ?? name}).`, "HOSTED_GATEWAY_FAILED", {
|
|
261
|
+
operation,
|
|
262
|
+
...code ? { gatewayCode: code } : {},
|
|
263
|
+
errorName: name
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
function hostedProviderConfig(gateway, apiKey, keyAlias) {
|
|
267
|
+
const binding = (model) => ({
|
|
268
|
+
provider: gateway.provider,
|
|
269
|
+
baseUrl: gateway.baseUrl,
|
|
270
|
+
apiKey,
|
|
271
|
+
...model ? { model } : {}
|
|
272
|
+
});
|
|
273
|
+
const tiers = {};
|
|
274
|
+
for (const [tier, model] of Object.entries(gateway.tiers ?? {})) if (model) tiers[tier] = binding(model);
|
|
275
|
+
return parseInferenceProviderConfig({
|
|
276
|
+
version: 1,
|
|
277
|
+
source: "hosted",
|
|
278
|
+
mode: "hosted",
|
|
279
|
+
gatewayKeyAlias: keyAlias,
|
|
280
|
+
...binding(gateway.model),
|
|
281
|
+
...Object.keys(tiers).length > 0 ? { tiers } : {}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function keyModels(gateway) {
|
|
285
|
+
if (gateway.models?.length) return gateway.models;
|
|
286
|
+
const models = /* @__PURE__ */ new Set();
|
|
287
|
+
if (gateway.model) models.add(gateway.model);
|
|
288
|
+
for (const model of Object.values(gateway.tiers ?? {})) if (model) models.add(model);
|
|
289
|
+
return models.size > 0 ? [...models] : void 0;
|
|
290
|
+
}
|
|
291
|
+
function restoredConfig(prior) {
|
|
292
|
+
if (prior && prior.mode !== "hosted" && prior.source !== "hosted") {
|
|
293
|
+
if (INFERENCE_TIERS.some((tier) => !!inferenceProviderBindingFor(prior, tier)) || prior.mode) return {
|
|
294
|
+
config: prior,
|
|
295
|
+
restored: "prior"
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
config: { mode: "off" },
|
|
300
|
+
restored: "off"
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function defaultNonce() {
|
|
304
|
+
return globalThis.crypto.randomUUID().replace(/-/g, "").slice(0, 10);
|
|
305
|
+
}
|
|
306
|
+
var lifecycleChain = Promise.resolve();
|
|
307
|
+
function createHostedInferenceProvisioner(options) {
|
|
308
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
309
|
+
const nonce = options.nonce ?? defaultNonce;
|
|
310
|
+
const deadlineMs = () => {
|
|
311
|
+
try {
|
|
312
|
+
return gatewayConfig().adminTimeoutMs ?? 3e4;
|
|
313
|
+
} catch {
|
|
314
|
+
return HOSTED_GATEWAY_ADMIN_TIMEOUT_MS;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
let secretsPromise;
|
|
318
|
+
let adminPromise;
|
|
319
|
+
let tablesPromise;
|
|
320
|
+
const gatewayConfig = () => options.gateway ? parseHostedGatewayConfig(options.gateway) : getHostedGatewayConfig();
|
|
321
|
+
const getSecrets = () => {
|
|
322
|
+
if (options.secrets) return Promise.resolve(options.secrets);
|
|
323
|
+
secretsPromise ??= SecretService.create({ db: options.db }).catch((error) => {
|
|
324
|
+
secretsPromise = void 0;
|
|
325
|
+
throw error;
|
|
326
|
+
});
|
|
327
|
+
return secretsPromise;
|
|
328
|
+
};
|
|
329
|
+
const getAdmin = (gateway) => {
|
|
330
|
+
if (options.admin) return Promise.resolve(options.admin);
|
|
331
|
+
adminPromise ??= createHostedGatewayAdmin(gateway).catch((error) => {
|
|
332
|
+
adminPromise = void 0;
|
|
333
|
+
throw error;
|
|
334
|
+
});
|
|
335
|
+
return adminPromise;
|
|
336
|
+
};
|
|
337
|
+
const optionalAdmin = async () => {
|
|
338
|
+
try {
|
|
339
|
+
return await getAdmin(gatewayConfig());
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (error instanceof HostedInferenceError && (error.code === "HOSTED_GATEWAY_NOT_CONFIGURED" || error.code === "HOSTED_GATEWAY_UNSUPPORTED")) return;
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
const ensureTables = () => {
|
|
346
|
+
tablesPromise ??= ensureSystemTables(options.db).then(() => void 0).catch((error) => {
|
|
347
|
+
tablesPromise = void 0;
|
|
348
|
+
throw error;
|
|
349
|
+
});
|
|
350
|
+
return tablesPromise;
|
|
351
|
+
};
|
|
352
|
+
const inTenant = (tenantId, fn) => withTenant({
|
|
353
|
+
tenantId,
|
|
354
|
+
...options.actor ? { userId: options.actor } : {}
|
|
355
|
+
}, fn);
|
|
356
|
+
const withTenantLock = (tenantId, fn) => {
|
|
357
|
+
const key = JSON.stringify(["smrt.inference.hosted", tenantId]);
|
|
358
|
+
const run = async () => {
|
|
359
|
+
if (isPostgresDatabase(options.db) && options.db.transaction) return options.db.transaction(async (tx) => {
|
|
360
|
+
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1::text, 0))", key);
|
|
361
|
+
return fn();
|
|
362
|
+
});
|
|
363
|
+
return fn();
|
|
364
|
+
};
|
|
365
|
+
const result = lifecycleChain.then(run, run);
|
|
366
|
+
lifecycleChain = result.then(() => void 0, () => void 0);
|
|
367
|
+
return result;
|
|
368
|
+
};
|
|
369
|
+
const read = async (secrets, name) => {
|
|
370
|
+
try {
|
|
371
|
+
return (await secrets.retrieve(name)).value;
|
|
372
|
+
} catch (error) {
|
|
373
|
+
if (isAbsentSecretError(error, name)) return void 0;
|
|
374
|
+
throw error;
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
const readState = async (secrets, userId) => {
|
|
378
|
+
const value = await read(secrets, hostedInferenceStateSecretName(userId));
|
|
379
|
+
return value === void 0 ? void 0 : parseState(value);
|
|
380
|
+
};
|
|
381
|
+
const readUserConfig = async (secrets, userId) => {
|
|
382
|
+
const value = await read(secrets, inferenceProviderConfigSecretName({ userId }));
|
|
383
|
+
return value === void 0 ? void 0 : parseInferenceProviderConfig(value);
|
|
384
|
+
};
|
|
385
|
+
const writeState = (secrets, userId, state, auditDetails) => secrets.store(hostedInferenceStateSecretName(userId), JSON.stringify(state), {
|
|
386
|
+
category: HOSTED_INFERENCE_SECRET_CATEGORY,
|
|
387
|
+
description: "Hosted inference provisioning state",
|
|
388
|
+
auditDetails
|
|
389
|
+
});
|
|
390
|
+
const writeUserConfig = (secrets, userId, config, auditDetails) => secrets.store(inferenceProviderConfigSecretName({ userId }), JSON.stringify(parseInferenceProviderConfig(config)), {
|
|
391
|
+
category: INFERENCE_PROVIDER_CONFIG_SECRET_TYPE,
|
|
392
|
+
description: "Inference provider config (user)",
|
|
393
|
+
auditDetails
|
|
394
|
+
});
|
|
395
|
+
const isOurs = (config, keyAlias) => !!keyAlias && config?.source === "hosted" && config.gatewayKeyAlias === keyAlias;
|
|
396
|
+
const deleteGatewayKey = async (admin, keyId) => {
|
|
397
|
+
if (!keyId || !admin) return "unsupported";
|
|
398
|
+
try {
|
|
399
|
+
return await withDeadline(() => admin.deleteVirtualKey(keyId), deadlineMs(), "deleteVirtualKey") ? "revoked" : "missing";
|
|
400
|
+
} catch {
|
|
401
|
+
return "failed";
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
const findTenantProject = async (tenantId, gateway, admin) => {
|
|
405
|
+
let projects;
|
|
406
|
+
try {
|
|
407
|
+
projects = await withDeadline(() => admin.listProjects({ tenantId }), deadlineMs(), "listProjects");
|
|
408
|
+
} catch (error) {
|
|
409
|
+
throw gatewayFailure("listProjects", error);
|
|
410
|
+
}
|
|
411
|
+
return (Array.isArray(projects) ? projects : []).filter((project) => project?.tenantId === tenantId && project.name === hostedProjectName(tenantId) && (gateway.provider !== "litellm" || project.id?.startsWith(`smrt-${tenantId}-`))).map((project) => text$1(project.id)).filter((id) => !!id).sort()[0];
|
|
412
|
+
};
|
|
413
|
+
const ensureProject = async (tenantId, gateway, admin, secrets) => {
|
|
414
|
+
const stored = await read(secrets, PROJECT_NAME);
|
|
415
|
+
if (stored !== void 0) try {
|
|
416
|
+
const parsed = JSON.parse(stored);
|
|
417
|
+
if (typeof parsed.projectId === "string" && parsed.provider === gateway.provider) return parsed.projectId;
|
|
418
|
+
} catch {}
|
|
419
|
+
return (await runOnce({
|
|
420
|
+
db: options.db,
|
|
421
|
+
tenantId,
|
|
422
|
+
actor: RUN_ONCE_ACTOR_PROJECT,
|
|
423
|
+
token: "smrt-inference-hosted:project",
|
|
424
|
+
content: {
|
|
425
|
+
provider: gateway.provider,
|
|
426
|
+
baseUrl: gateway.baseUrl
|
|
427
|
+
}
|
|
428
|
+
}, async () => {
|
|
429
|
+
const found = await findTenantProject(tenantId, gateway, admin);
|
|
430
|
+
let projectId = found;
|
|
431
|
+
if (!projectId) {
|
|
432
|
+
let project;
|
|
433
|
+
try {
|
|
434
|
+
project = await withDeadline(() => admin.createProject({
|
|
435
|
+
id: `smrt-${tenantId}-${nonce()}`,
|
|
436
|
+
name: hostedProjectName(tenantId),
|
|
437
|
+
tenantId,
|
|
438
|
+
metadata: {
|
|
439
|
+
smrt: HOSTED_INFERENCE_FEATURE_KEY,
|
|
440
|
+
tenantId
|
|
441
|
+
}
|
|
442
|
+
}), deadlineMs(), "createProject");
|
|
443
|
+
} catch (error) {
|
|
444
|
+
throw gatewayFailure("createProject", error);
|
|
445
|
+
}
|
|
446
|
+
projectId = text$1(project?.id);
|
|
447
|
+
}
|
|
448
|
+
if (!projectId) throw new HostedInferenceError("Gateway createProject returned no project id.", "HOSTED_GATEWAY_FAILED", { operation: "createProject" });
|
|
449
|
+
await secrets.store(PROJECT_NAME, JSON.stringify({
|
|
450
|
+
projectId,
|
|
451
|
+
provider: gateway.provider,
|
|
452
|
+
createdAt: now().toISOString()
|
|
453
|
+
}), {
|
|
454
|
+
category: HOSTED_INFERENCE_SECRET_CATEGORY,
|
|
455
|
+
description: "Hosted inference gateway project",
|
|
456
|
+
auditDetails: {
|
|
457
|
+
operation: "inference.hosted.project",
|
|
458
|
+
projectId,
|
|
459
|
+
provider: gateway.provider,
|
|
460
|
+
...found ? { adopted: true } : {}
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
return { projectId };
|
|
464
|
+
})).projectId;
|
|
465
|
+
};
|
|
466
|
+
const retryPending = async (state) => {
|
|
467
|
+
const pending = state.pendingGatewayRevocations ?? [];
|
|
468
|
+
if (!pending.some((entry) => entry.keyId)) return {
|
|
469
|
+
state,
|
|
470
|
+
settled: []
|
|
471
|
+
};
|
|
472
|
+
const admin = await optionalAdmin();
|
|
473
|
+
const remaining = [];
|
|
474
|
+
const settled = [];
|
|
475
|
+
for (const entry of pending) {
|
|
476
|
+
const outcome = await deleteGatewayKey(admin, entry.keyId);
|
|
477
|
+
if (outcome === "revoked" || outcome === "missing") settled.push({
|
|
478
|
+
keyId: entry.keyId,
|
|
479
|
+
keyAlias: entry.keyAlias,
|
|
480
|
+
gateway: outcome
|
|
481
|
+
});
|
|
482
|
+
else remaining.push(entry);
|
|
483
|
+
}
|
|
484
|
+
if (settled.length === 0) return {
|
|
485
|
+
state,
|
|
486
|
+
settled
|
|
487
|
+
};
|
|
488
|
+
return {
|
|
489
|
+
state: {
|
|
490
|
+
...state,
|
|
491
|
+
pendingGatewayRevocations: remaining
|
|
492
|
+
},
|
|
493
|
+
settled
|
|
494
|
+
};
|
|
495
|
+
};
|
|
496
|
+
const revokeLocked = async (tenantId, userId, secrets, state, reason, restore, optOut = false) => {
|
|
497
|
+
const since = now().toISOString();
|
|
498
|
+
const audit = {
|
|
499
|
+
operation: "inference.hosted.revoke",
|
|
500
|
+
reason,
|
|
501
|
+
keyAlias: state.keyAlias,
|
|
502
|
+
...state.keyId ? { keyId: state.keyId } : {}
|
|
503
|
+
};
|
|
504
|
+
let restored = "kept";
|
|
505
|
+
if (restore) {
|
|
506
|
+
const current = await readUserConfig(secrets, userId);
|
|
507
|
+
if (isOurs(current, state.keyAlias)) {
|
|
508
|
+
const next2 = restoredConfig(state.prior);
|
|
509
|
+
restored = next2.restored;
|
|
510
|
+
await writeUserConfig(secrets, userId, next2.config, {
|
|
511
|
+
...audit,
|
|
512
|
+
restored
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const owedEntry = {
|
|
517
|
+
keyId: state.keyId,
|
|
518
|
+
keyAlias: state.keyAlias,
|
|
519
|
+
reason: "failed",
|
|
520
|
+
since
|
|
521
|
+
};
|
|
522
|
+
const retired = {
|
|
523
|
+
...state,
|
|
524
|
+
status: "revoked",
|
|
525
|
+
generation: state.generation + 1,
|
|
526
|
+
revokedAt: since,
|
|
527
|
+
prior: null,
|
|
528
|
+
...optOut ? { optedOut: true } : {},
|
|
529
|
+
pendingGatewayRevocations: [...state.pendingGatewayRevocations ?? [], owedEntry]
|
|
530
|
+
};
|
|
531
|
+
await writeState(secrets, userId, retired, {
|
|
532
|
+
...audit,
|
|
533
|
+
restored,
|
|
534
|
+
gateway: "pending"
|
|
535
|
+
});
|
|
536
|
+
const gateway = await deleteGatewayKey(await optionalAdmin(), state.keyId);
|
|
537
|
+
const owed = gateway === "unsupported" || gateway === "failed";
|
|
538
|
+
const next = {
|
|
539
|
+
...retired,
|
|
540
|
+
pendingGatewayRevocations: [...state.pendingGatewayRevocations ?? [], ...owed ? [{
|
|
541
|
+
...owedEntry,
|
|
542
|
+
reason: gateway
|
|
543
|
+
}] : []]
|
|
544
|
+
};
|
|
545
|
+
await writeState(secrets, userId, next, {
|
|
546
|
+
...audit,
|
|
547
|
+
restored,
|
|
548
|
+
gateway,
|
|
549
|
+
...owed ? { manualGatewayRevocation: true } : {}
|
|
550
|
+
});
|
|
551
|
+
return {
|
|
552
|
+
...describeState(tenantId, userId, next),
|
|
553
|
+
revoked: true,
|
|
554
|
+
gateway,
|
|
555
|
+
restored
|
|
556
|
+
};
|
|
557
|
+
};
|
|
558
|
+
const provisionLocked = async (tenantId, userId, budget, gateway, secrets) => {
|
|
559
|
+
let state = await readState(secrets, userId);
|
|
560
|
+
if (state?.status === "active") {
|
|
561
|
+
const current = await readUserConfig(secrets, userId);
|
|
562
|
+
if (isOurs(current, state.keyAlias)) {
|
|
563
|
+
const sync = sameBudget(state.budget, budget) ? void 0 : await syncLocked(tenantId, userId, budget, secrets);
|
|
564
|
+
return {
|
|
565
|
+
...describeState(tenantId, userId, await readState(secrets, userId) ?? state),
|
|
566
|
+
created: false,
|
|
567
|
+
...sync && sync.sync !== "none" ? { budgetSync: sync.sync } : {}
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
await revokeLocked(tenantId, userId, secrets, state, "user-replaced", false, true);
|
|
571
|
+
state = await readState(secrets, userId);
|
|
572
|
+
}
|
|
573
|
+
if (options.entitled && !await options.entitled(tenantId)) throw new HostedInferenceError(`Tenant ${tenantId} is not entitled to ${HOSTED_INFERENCE_FEATURE_KEY}; nothing was provisioned.`, "HOSTED_NOT_ENTITLED", {
|
|
574
|
+
tenantId,
|
|
575
|
+
userId
|
|
576
|
+
});
|
|
577
|
+
const generation = state?.generation ?? 0;
|
|
578
|
+
const admin = await getAdmin(gateway);
|
|
579
|
+
const projectId = await ensureProject(tenantId, gateway, admin, secrets);
|
|
580
|
+
let rollback;
|
|
581
|
+
const provisioned = await runOnce({
|
|
582
|
+
db: options.db,
|
|
583
|
+
tenantId,
|
|
584
|
+
actor: userId,
|
|
585
|
+
token: "smrt-inference-hosted:provision",
|
|
586
|
+
content: {
|
|
587
|
+
userId,
|
|
588
|
+
generation
|
|
589
|
+
}
|
|
590
|
+
}, async () => {
|
|
591
|
+
const current = await readUserConfig(secrets, userId);
|
|
592
|
+
const prior = current && current.source !== "hosted" ? current : null;
|
|
593
|
+
const keyAlias = `smrt-${tenantId}-${userId}-g${generation}-${nonce()}`;
|
|
594
|
+
const models = keyModels(gateway);
|
|
595
|
+
const request = {
|
|
596
|
+
name: keyAlias,
|
|
597
|
+
description: `smrt hosted inference for user ${userId}`,
|
|
598
|
+
projectId,
|
|
599
|
+
tenantId,
|
|
600
|
+
userId,
|
|
601
|
+
budget: toGatewayBudget(budget),
|
|
602
|
+
...models ? { models } : {},
|
|
603
|
+
...gateway.keyDuration ? { duration: gateway.keyDuration } : {},
|
|
604
|
+
...gateway.provider === "bifrost" ? { keyIds: ["*"] } : {},
|
|
605
|
+
metadata: {
|
|
606
|
+
smrt: HOSTED_INFERENCE_FEATURE_KEY,
|
|
607
|
+
tenantId,
|
|
608
|
+
userId,
|
|
609
|
+
generation
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
let key;
|
|
613
|
+
let createError;
|
|
614
|
+
try {
|
|
615
|
+
key = await withDeadline(() => admin.createVirtualKey(request), deadlineMs(), "createVirtualKey");
|
|
616
|
+
} catch (error) {
|
|
617
|
+
createError = error;
|
|
618
|
+
}
|
|
619
|
+
const apiKey = text$1(key?.key);
|
|
620
|
+
const keyId = safeKeyId(key?.id, apiKey);
|
|
621
|
+
const scheduleRollback = () => {
|
|
622
|
+
const previous = state;
|
|
623
|
+
rollback = async () => {
|
|
624
|
+
const outcome = await deleteGatewayKey(admin, keyId);
|
|
625
|
+
const owed = outcome === "unsupported" || outcome === "failed";
|
|
626
|
+
const audit2 = {
|
|
627
|
+
operation: "inference.hosted.provision-rollback",
|
|
628
|
+
keyAlias,
|
|
629
|
+
gateway: outcome,
|
|
630
|
+
...keyId ? { keyId } : {},
|
|
631
|
+
...owed ? { manualGatewayRevocation: true } : {}
|
|
632
|
+
};
|
|
633
|
+
const pending = [...previous?.pendingGatewayRevocations ?? [], ...owed ? [{
|
|
634
|
+
keyId,
|
|
635
|
+
keyAlias,
|
|
636
|
+
reason: "orphaned",
|
|
637
|
+
since: now().toISOString()
|
|
638
|
+
}] : []];
|
|
639
|
+
if (previous) await writeState(secrets, userId, {
|
|
640
|
+
...previous,
|
|
641
|
+
pendingGatewayRevocations: pending
|
|
642
|
+
}, audit2);
|
|
643
|
+
else if (owed) await writeState(secrets, userId, {
|
|
644
|
+
version: 1,
|
|
645
|
+
status: "revoked",
|
|
646
|
+
generation,
|
|
647
|
+
keyId: null,
|
|
648
|
+
keyAlias: "",
|
|
649
|
+
prior: null,
|
|
650
|
+
pendingGatewayRevocations: pending
|
|
651
|
+
}, audit2);
|
|
652
|
+
else await secrets.delete(hostedInferenceStateSecretName(userId), { auditDetails: audit2 });
|
|
653
|
+
};
|
|
654
|
+
};
|
|
655
|
+
if (createError !== void 0) {
|
|
656
|
+
if (mayHaveCreated(createError)) scheduleRollback();
|
|
657
|
+
throw gatewayFailure("createVirtualKey", createError);
|
|
658
|
+
}
|
|
659
|
+
if (!apiKey) {
|
|
660
|
+
scheduleRollback();
|
|
661
|
+
throw new HostedInferenceError("Gateway createVirtualKey returned no key value.", "HOSTED_GATEWAY_FAILED", { operation: "createVirtualKey" });
|
|
662
|
+
}
|
|
663
|
+
const next = {
|
|
664
|
+
version: 1,
|
|
665
|
+
status: "active",
|
|
666
|
+
generation,
|
|
667
|
+
provider: gateway.provider,
|
|
668
|
+
projectId,
|
|
669
|
+
keyId,
|
|
670
|
+
keyAlias,
|
|
671
|
+
budget,
|
|
672
|
+
provisionedAt: now().toISOString(),
|
|
673
|
+
prior,
|
|
674
|
+
...state?.pendingGatewayRevocations?.length ? { pendingGatewayRevocations: state.pendingGatewayRevocations } : {}
|
|
675
|
+
};
|
|
676
|
+
const audit = {
|
|
677
|
+
operation: "inference.hosted.provision",
|
|
678
|
+
keyAlias,
|
|
679
|
+
projectId,
|
|
680
|
+
provider: gateway.provider,
|
|
681
|
+
...keyId ? { keyId } : {}
|
|
682
|
+
};
|
|
683
|
+
try {
|
|
684
|
+
await writeState(secrets, userId, next, audit);
|
|
685
|
+
await writeUserConfig(secrets, userId, hostedProviderConfig(gateway, apiKey, keyAlias), audit);
|
|
686
|
+
} catch (error) {
|
|
687
|
+
scheduleRollback();
|
|
688
|
+
throw error;
|
|
689
|
+
}
|
|
690
|
+
return {
|
|
691
|
+
...describeState(tenantId, userId, next),
|
|
692
|
+
created: true
|
|
693
|
+
};
|
|
694
|
+
}).catch(async (error) => {
|
|
695
|
+
if (rollback) await rollback().catch(() => void 0);
|
|
696
|
+
throw error;
|
|
697
|
+
});
|
|
698
|
+
const stored = await readUserConfig(secrets, userId);
|
|
699
|
+
if (!isOurs(stored, provisioned.keyAlias ?? "")) throw new HostedInferenceError("Hosted inference state for this user was removed outside provisioning; revoke the gateway key named in the audit log and provision again after the run-once claim expires.", "HOSTED_STATE_INVALID", {
|
|
700
|
+
tenantId,
|
|
701
|
+
userId,
|
|
702
|
+
keyAlias: provisioned.keyAlias
|
|
703
|
+
});
|
|
704
|
+
return provisioned;
|
|
705
|
+
};
|
|
706
|
+
const syncLocked = async (tenantId, userId, budget, secrets) => {
|
|
707
|
+
const state = await readState(secrets, userId);
|
|
708
|
+
if (state?.status !== "active") return {
|
|
709
|
+
...describeState(tenantId, userId, state),
|
|
710
|
+
sync: "none"
|
|
711
|
+
};
|
|
712
|
+
const uncertainAt = state.budgetSyncUncertainAt;
|
|
713
|
+
if (!uncertainAt && sameBudget(state.budget, budget)) return {
|
|
714
|
+
...describeState(tenantId, userId, state),
|
|
715
|
+
sync: "unchanged"
|
|
716
|
+
};
|
|
717
|
+
if (!state.keyId) return {
|
|
718
|
+
...describeState(tenantId, userId, state),
|
|
719
|
+
sync: "unsupported"
|
|
720
|
+
};
|
|
721
|
+
const admin = await getAdmin(gatewayConfig());
|
|
722
|
+
try {
|
|
723
|
+
const keyId = state.keyId;
|
|
724
|
+
await withDeadline(() => admin.updateVirtualKey(keyId, { budget: toGatewayBudget(budget) }), deadlineMs(), "updateVirtualKey");
|
|
725
|
+
} catch (error) {
|
|
726
|
+
await writeState(secrets, userId, {
|
|
727
|
+
...state,
|
|
728
|
+
budgetSyncUncertainAt: now().toISOString()
|
|
729
|
+
}, {
|
|
730
|
+
operation: "inference.hosted.budget-uncertain",
|
|
731
|
+
keyAlias: state.keyAlias,
|
|
732
|
+
keyId: state.keyId
|
|
733
|
+
});
|
|
734
|
+
throw gatewayFailure("updateVirtualKey", error);
|
|
735
|
+
}
|
|
736
|
+
const settled = !uncertainAt || now().getTime() - Date.parse(uncertainAt) >= 9e5;
|
|
737
|
+
const { budgetSyncUncertainAt: _settled, ...rest } = state;
|
|
738
|
+
const next = {
|
|
739
|
+
...settled ? rest : state,
|
|
740
|
+
budget
|
|
741
|
+
};
|
|
742
|
+
if (sameBudget(state.budget, budget) && !(settled && uncertainAt)) return {
|
|
743
|
+
...describeState(tenantId, userId, next),
|
|
744
|
+
sync: "updated"
|
|
745
|
+
};
|
|
746
|
+
await writeState(secrets, userId, next, {
|
|
747
|
+
operation: "inference.hosted.budget",
|
|
748
|
+
keyAlias: state.keyAlias,
|
|
749
|
+
keyId: state.keyId,
|
|
750
|
+
limitMicros: budget.limitMicros,
|
|
751
|
+
window: budget.window
|
|
752
|
+
});
|
|
753
|
+
return {
|
|
754
|
+
...describeState(tenantId, userId, next),
|
|
755
|
+
sync: "updated"
|
|
756
|
+
};
|
|
757
|
+
};
|
|
758
|
+
const listStates = async (tenantId) => {
|
|
759
|
+
const secrets = await getSecrets();
|
|
760
|
+
return inTenant(tenantId, async () => {
|
|
761
|
+
const rows = await secrets.list({ category: HOSTED_INFERENCE_SECRET_CATEGORY });
|
|
762
|
+
const states = [];
|
|
763
|
+
for (const row of rows) {
|
|
764
|
+
if (!row.name.startsWith(STATE_PREFIX)) continue;
|
|
765
|
+
const userId = row.name.slice(STATE_PREFIX.length);
|
|
766
|
+
const state = await readState(secrets, userId);
|
|
767
|
+
if (state) states.push({
|
|
768
|
+
userId,
|
|
769
|
+
state
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
return states.sort((a, b) => a.userId.localeCompare(b.userId));
|
|
773
|
+
});
|
|
774
|
+
};
|
|
775
|
+
const retryPendingLocked = async (userId, secrets) => {
|
|
776
|
+
const state = await readState(secrets, userId);
|
|
777
|
+
if (!state?.pendingGatewayRevocations?.length) return state;
|
|
778
|
+
const { state: retried, settled } = await retryPending(state);
|
|
779
|
+
if (settled.length === 0) return state;
|
|
780
|
+
await writeState(secrets, userId, retried, {
|
|
781
|
+
operation: "inference.hosted.revoke-retry",
|
|
782
|
+
settled,
|
|
783
|
+
remaining: retried.pendingGatewayRevocations?.length ?? 0
|
|
784
|
+
});
|
|
785
|
+
return retried;
|
|
786
|
+
};
|
|
787
|
+
const revokeUserLocked = async (tenantId, userId, secrets, reason) => {
|
|
788
|
+
const state = await readState(secrets, userId);
|
|
789
|
+
if (state?.status !== "active") return {
|
|
790
|
+
...describeState(tenantId, userId, await retryPendingLocked(userId, secrets)),
|
|
791
|
+
revoked: false,
|
|
792
|
+
gateway: "none"
|
|
793
|
+
};
|
|
794
|
+
return revokeLocked(tenantId, userId, secrets, state, reason, true);
|
|
795
|
+
};
|
|
796
|
+
const reconcileUserLocked = async (tenantId, userId, budget, gateway, secrets, result) => {
|
|
797
|
+
const state = await readState(secrets, userId);
|
|
798
|
+
if (state?.status === "revoked" && state.optedOut) return;
|
|
799
|
+
if (state?.status === "active") {
|
|
800
|
+
const current = await readUserConfig(secrets, userId);
|
|
801
|
+
if (!isOurs(current, state.keyAlias)) {
|
|
802
|
+
result.revoked.push(await revokeLocked(tenantId, userId, secrets, state, "user-replaced", false, true));
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
result.synced.push(await syncLocked(tenantId, userId, budget, secrets));
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
result.provisioned.push(await provisionLocked(tenantId, userId, budget, gateway, secrets));
|
|
809
|
+
};
|
|
810
|
+
const locked = async (tenantId, fn, invalidate) => {
|
|
811
|
+
await ensureTables();
|
|
812
|
+
const secrets = await getSecrets();
|
|
813
|
+
try {
|
|
814
|
+
return await withTenantLock(tenantId, () => inTenant(tenantId, () => fn(secrets)));
|
|
815
|
+
} finally {
|
|
816
|
+
if (invalidate) invalidateUserInferenceResolvers({
|
|
817
|
+
tenantId,
|
|
818
|
+
...invalidate.userId ? { userId: invalidate.userId } : {}
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
return {
|
|
823
|
+
async provision(input) {
|
|
824
|
+
const tenantId = requireId(input.tenantId, "tenantId");
|
|
825
|
+
const userId = requireId(input.userId, "userId");
|
|
826
|
+
const budget = normalizeHostedBudget(input.budget);
|
|
827
|
+
const gateway = gatewayConfig();
|
|
828
|
+
return locked(tenantId, (secrets) => provisionLocked(tenantId, userId, budget, gateway, secrets), { userId });
|
|
829
|
+
},
|
|
830
|
+
async revoke(input) {
|
|
831
|
+
const tenantId = requireId(input.tenantId, "tenantId");
|
|
832
|
+
const userId = requireId(input.userId, "userId");
|
|
833
|
+
return locked(tenantId, (secrets) => revokeUserLocked(tenantId, userId, secrets, text$1(input.reason) ?? "revoke"), { userId });
|
|
834
|
+
},
|
|
835
|
+
async syncBudget(input) {
|
|
836
|
+
const tenantId = requireId(input.tenantId, "tenantId");
|
|
837
|
+
const userId = requireId(input.userId, "userId");
|
|
838
|
+
const budget = normalizeHostedBudget(input.budget);
|
|
839
|
+
return locked(tenantId, (secrets) => syncLocked(tenantId, userId, budget, secrets));
|
|
840
|
+
},
|
|
841
|
+
async status(input) {
|
|
842
|
+
const tenantId = requireId(input.tenantId, "tenantId");
|
|
843
|
+
const userId = requireId(input.userId, "userId");
|
|
844
|
+
const secrets = await getSecrets();
|
|
845
|
+
return inTenant(tenantId, async () => describeState(tenantId, userId, await readState(secrets, userId)));
|
|
846
|
+
},
|
|
847
|
+
async listActive(tenantIdInput) {
|
|
848
|
+
const tenantId = requireId(tenantIdInput, "tenantId");
|
|
849
|
+
return (await listStates(tenantId)).filter(({ state }) => state.status === "active").map(({ userId }) => userId);
|
|
850
|
+
},
|
|
851
|
+
async listKnown(tenantIdInput) {
|
|
852
|
+
const tenantId = requireId(tenantIdInput, "tenantId");
|
|
853
|
+
return (await listStates(tenantId)).map(({ userId }) => userId);
|
|
854
|
+
},
|
|
855
|
+
async revokeTenant(tenantIdInput, revokeOptions = {}) {
|
|
856
|
+
const tenantId = requireId(tenantIdInput, "tenantId");
|
|
857
|
+
return locked(tenantId, async (secrets) => {
|
|
858
|
+
const results = [];
|
|
859
|
+
for (const { userId, state } of await listStates(tenantId)) {
|
|
860
|
+
if (state.status !== "active") continue;
|
|
861
|
+
results.push(await revokeUserLocked(tenantId, userId, secrets, text$1(revokeOptions.reason) ?? "revoke"));
|
|
862
|
+
}
|
|
863
|
+
return results;
|
|
864
|
+
}, {});
|
|
865
|
+
},
|
|
866
|
+
async syncTenantBudget(tenantIdInput, budgetInput) {
|
|
867
|
+
const tenantId = requireId(tenantIdInput, "tenantId");
|
|
868
|
+
const budget = normalizeHostedBudget(budgetInput);
|
|
869
|
+
return locked(tenantId, async (secrets) => {
|
|
870
|
+
const results = [];
|
|
871
|
+
for (const { userId, state } of await listStates(tenantId)) {
|
|
872
|
+
if (state.status !== "active") continue;
|
|
873
|
+
results.push(await syncLocked(tenantId, userId, budget, secrets));
|
|
874
|
+
}
|
|
875
|
+
return results;
|
|
876
|
+
});
|
|
877
|
+
},
|
|
878
|
+
async acknowledgeGatewayRevocation(input) {
|
|
879
|
+
const tenantId = requireId(input.tenantId, "tenantId");
|
|
880
|
+
const userId = requireId(input.userId, "userId");
|
|
881
|
+
const keyAlias = text$1(input.keyAlias);
|
|
882
|
+
if (!keyAlias) throw new HostedInferenceError("keyAlias is required", "HOSTED_INFERENCE_INVALID", { field: "keyAlias" });
|
|
883
|
+
return locked(tenantId, async (secrets) => {
|
|
884
|
+
const state = await readState(secrets, userId);
|
|
885
|
+
const pending = state?.pendingGatewayRevocations ?? [];
|
|
886
|
+
const remaining = pending.filter((entry) => entry.keyAlias !== keyAlias);
|
|
887
|
+
if (!state || remaining.length === pending.length) return false;
|
|
888
|
+
await writeState(secrets, userId, {
|
|
889
|
+
...state,
|
|
890
|
+
pendingGatewayRevocations: remaining
|
|
891
|
+
}, {
|
|
892
|
+
operation: "inference.hosted.revoke-acknowledged",
|
|
893
|
+
keyAlias
|
|
894
|
+
});
|
|
895
|
+
return true;
|
|
896
|
+
});
|
|
897
|
+
},
|
|
898
|
+
async reconcileTenant(tenantIdInput, target) {
|
|
899
|
+
const tenantId = requireId(tenantIdInput, "tenantId");
|
|
900
|
+
const budget = target.budget ? normalizeHostedBudget(target.budget) : void 0;
|
|
901
|
+
const users = (target.users ?? []).map((userId) => requireId(userId, "userId"));
|
|
902
|
+
if (target.entitled && users.length > 0 && !budget) throw new HostedInferenceError("reconcileTenant needs a budget to provision users.", "HOSTED_INFERENCE_INVALID", { tenantId });
|
|
903
|
+
const gateway = target.entitled && users.length > 0 ? gatewayConfig() : void 0;
|
|
904
|
+
return locked(tenantId, async (secrets) => {
|
|
905
|
+
const result = {
|
|
906
|
+
tenantId,
|
|
907
|
+
provisioned: [],
|
|
908
|
+
revoked: [],
|
|
909
|
+
synced: [],
|
|
910
|
+
pending: [],
|
|
911
|
+
failures: []
|
|
912
|
+
};
|
|
913
|
+
const attempt = async (userId, fn) => {
|
|
914
|
+
try {
|
|
915
|
+
await fn();
|
|
916
|
+
} catch (error) {
|
|
917
|
+
result.failures.push({
|
|
918
|
+
userId,
|
|
919
|
+
error
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
const states = await listStates(tenantId);
|
|
924
|
+
if (!(target.entitled && (!options.entitled || await options.entitled(tenantId)))) for (const { userId, state } of states) {
|
|
925
|
+
if (state.status !== "active") continue;
|
|
926
|
+
await attempt(userId, async () => {
|
|
927
|
+
result.revoked.push(await revokeLocked(tenantId, userId, secrets, state, "entitlement-lost", true));
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
else {
|
|
931
|
+
const wanted = new Set(users);
|
|
932
|
+
for (const userId of wanted) await attempt(userId, () => reconcileUserLocked(tenantId, userId, budget, gateway, secrets, result));
|
|
933
|
+
if (budget) for (const { userId, state } of states) {
|
|
934
|
+
if (wanted.has(userId) || state.status !== "active") continue;
|
|
935
|
+
await attempt(userId, async () => {
|
|
936
|
+
result.synced.push(await syncLocked(tenantId, userId, budget, secrets));
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
for (const { userId } of await listStates(tenantId)) await attempt(userId, async () => {
|
|
941
|
+
const remaining = (await retryPendingLocked(userId, secrets))?.pendingGatewayRevocations?.length ?? 0;
|
|
942
|
+
if (remaining > 0) result.pending.push({
|
|
943
|
+
userId,
|
|
944
|
+
remaining
|
|
945
|
+
});
|
|
946
|
+
});
|
|
947
|
+
return result;
|
|
948
|
+
}, {});
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
function provisionHostedInference(options) {
|
|
953
|
+
const { tenantId, userId, budget, ...deps } = options;
|
|
954
|
+
return createHostedInferenceProvisioner(deps).provision({
|
|
955
|
+
tenantId,
|
|
956
|
+
userId,
|
|
957
|
+
budget
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
function revokeHostedInference(options) {
|
|
961
|
+
const { tenantId, userId, reason, ...deps } = options;
|
|
962
|
+
return createHostedInferenceProvisioner(deps).revoke({
|
|
963
|
+
tenantId,
|
|
964
|
+
userId,
|
|
965
|
+
...reason ? { reason } : {}
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
function hostedBudgetFromGrantMetadata(metadata) {
|
|
969
|
+
const budget = metadata?.budget;
|
|
970
|
+
if (!budget || typeof budget !== "object") return void 0;
|
|
971
|
+
const { limitMicros, window } = budget;
|
|
972
|
+
if (typeof limitMicros !== "number") return void 0;
|
|
973
|
+
return {
|
|
974
|
+
limitMicros,
|
|
975
|
+
...typeof window === "string" ? { window } : {}
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
function hostedGrant(snapshot) {
|
|
979
|
+
return snapshot.grants.find((grant) => grant.featureKey === "inference.hosted" && grant.enabled !== false);
|
|
980
|
+
}
|
|
981
|
+
function throwReconcileFailures(result) {
|
|
982
|
+
if (result.failures.length === 0) return;
|
|
983
|
+
throw new HostedInferenceError(`Hosted inference failed for ${result.failures.length} user(s) of tenant ${result.tenantId}.`, "HOSTED_GATEWAY_FAILED", {
|
|
984
|
+
tenantId: result.tenantId,
|
|
985
|
+
users: result.failures.map((failure) => failure.userId),
|
|
986
|
+
codes: result.failures.map((failure) => failure.error instanceof SmrtError ? failure.error.code : failure.error instanceof Error ? failure.error.name : "unknown")
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
function createHostedInferenceReconciler(options) {
|
|
990
|
+
return async (tenantId, snapshot) => {
|
|
991
|
+
const grant = hostedGrant(snapshot);
|
|
992
|
+
if (!grant) {
|
|
993
|
+
const result2 = await options.provisioner.reconcileTenant(tenantId, { entitled: false });
|
|
994
|
+
throwReconcileFailures(result2);
|
|
995
|
+
return result2;
|
|
996
|
+
}
|
|
997
|
+
const budget = options.budgetFor?.(tenantId, snapshot) ?? hostedBudgetFromGrantMetadata(grant.metadata) ?? options.defaultBudget;
|
|
998
|
+
if (!budget) throw new HostedInferenceError(`The ${HOSTED_INFERENCE_FEATURE_KEY} grant names no budget (metadata.budget.limitMicros) and no defaultBudget is set.`, "HOSTED_INFERENCE_INVALID", { tenantId });
|
|
999
|
+
const result = await options.provisioner.reconcileTenant(tenantId, {
|
|
1000
|
+
entitled: true,
|
|
1001
|
+
budget,
|
|
1002
|
+
users: await options.users(tenantId)
|
|
1003
|
+
});
|
|
1004
|
+
throwReconcileFailures(result);
|
|
1005
|
+
return result;
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
function createHostedInferenceEntitlementListener(options) {
|
|
1009
|
+
const reconcile = createHostedInferenceReconciler(options);
|
|
1010
|
+
return async (change) => {
|
|
1011
|
+
if (![
|
|
1012
|
+
change.gained,
|
|
1013
|
+
change.lost,
|
|
1014
|
+
change.changed
|
|
1015
|
+
].some((keys) => keys.includes("inference.hosted"))) return;
|
|
1016
|
+
await reconcile(change.tenantId, change.after);
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
//#endregion
|
|
1020
|
+
//#region src/node.ts
|
|
1021
|
+
var DEFAULT_PROBE_TIMEOUT_MS = 3e3;
|
|
1022
|
+
var OPENAI_DEFAULT_BASE = "https://api.openai.com/v1";
|
|
1023
|
+
var ANTHROPIC_DEFAULT_BASE = "https://api.anthropic.com/v1";
|
|
1024
|
+
var GEMINI_DEFAULT_BASE = "https://generativelanguage.googleapis.com/v1beta";
|
|
1025
|
+
var OLLAMA_DEFAULT_BASE = "http://127.0.0.1:11434";
|
|
1026
|
+
var OPENAI_COMPATIBLE = /* @__PURE__ */ new Set([
|
|
1027
|
+
"openai",
|
|
1028
|
+
"litellm",
|
|
1029
|
+
"bifrost"
|
|
1030
|
+
]);
|
|
1031
|
+
function text(value) {
|
|
1032
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
1033
|
+
}
|
|
1034
|
+
function worst(statuses) {
|
|
1035
|
+
if (statuses.includes("error")) return "error";
|
|
1036
|
+
if (statuses.includes("warn")) return "warn";
|
|
1037
|
+
return "ok";
|
|
1038
|
+
}
|
|
1039
|
+
function keySource(candidate) {
|
|
1040
|
+
if (text(candidate.apiKey)) return {
|
|
1041
|
+
inline: true,
|
|
1042
|
+
required: true
|
|
1043
|
+
};
|
|
1044
|
+
const env = text(candidate.apiKeyEnv);
|
|
1045
|
+
if (env) return {
|
|
1046
|
+
env,
|
|
1047
|
+
required: true
|
|
1048
|
+
};
|
|
1049
|
+
const preset = text(candidate.preset);
|
|
1050
|
+
if (preset && isInferencePresetId(preset)) return { required: INFERENCE_PRESETS[preset].requiresApiKey };
|
|
1051
|
+
if (text(candidate.baseUrl)) return { required: false };
|
|
1052
|
+
const providerEnv = getDefaultAIKeyEnvName(candidate.provider);
|
|
1053
|
+
return providerEnv ? {
|
|
1054
|
+
env: providerEnv,
|
|
1055
|
+
required: true,
|
|
1056
|
+
providerDefault: true
|
|
1057
|
+
} : { required: false };
|
|
1058
|
+
}
|
|
1059
|
+
function resolveKey(candidate, env) {
|
|
1060
|
+
const source = keySource(candidate);
|
|
1061
|
+
if (source.inline) return text(candidate.apiKey);
|
|
1062
|
+
return source.env ? text(env[source.env]) : void 0;
|
|
1063
|
+
}
|
|
1064
|
+
function checkCandidateOffline(candidate, options) {
|
|
1065
|
+
const label = candidateLabel(candidate);
|
|
1066
|
+
const problems = [];
|
|
1067
|
+
const statuses = [];
|
|
1068
|
+
const fail = (message) => {
|
|
1069
|
+
problems.push(message);
|
|
1070
|
+
statuses.push("error");
|
|
1071
|
+
};
|
|
1072
|
+
const preset = text(candidate.preset);
|
|
1073
|
+
const provider = text(candidate.provider);
|
|
1074
|
+
if (preset) {
|
|
1075
|
+
if (!isInferencePresetId(preset)) fail(`unknown preset "${preset}"`);
|
|
1076
|
+
} else if (provider) {
|
|
1077
|
+
if (!options.providers.includes(provider.toLowerCase())) fail(`provider "${provider}" is not recognized`);
|
|
1078
|
+
}
|
|
1079
|
+
if (!text(candidate.model)) {
|
|
1080
|
+
problems.push("no model named (the provider default answers)");
|
|
1081
|
+
statuses.push("warn");
|
|
1082
|
+
}
|
|
1083
|
+
const source = keySource(candidate);
|
|
1084
|
+
if (source.required && !resolveKey(candidate, options.env)) if (source.providerDefault) {
|
|
1085
|
+
problems.push(`key variable ${source.env} is not set (fine if the configured client is this provider)`);
|
|
1086
|
+
statuses.push("warn");
|
|
1087
|
+
} else fail(source.env ? `key variable ${source.env} is not set` : "a key is required and none is configured");
|
|
1088
|
+
if ((text(candidate.apiKeyEnv) || text(candidate.apiKey)) && !preset) {
|
|
1089
|
+
if (!text(candidate.baseUrl)) fail("brings its own key without a baseUrl");
|
|
1090
|
+
}
|
|
1091
|
+
const sdkWide = ["HAVE_AI_BASE_URL", "HAVE_AI_API_KEY"].find((name) => text(options.env[name]));
|
|
1092
|
+
if (sdkWide && provider && !preset && !text(candidate.baseUrl)) {
|
|
1093
|
+
problems.push(`${sdkWide} is set: core refuses this candidate unless ${provider} is the configured provider (give it a baseUrl and apiKeyEnv)`);
|
|
1094
|
+
statuses.push("warn");
|
|
1095
|
+
}
|
|
1096
|
+
return {
|
|
1097
|
+
label,
|
|
1098
|
+
status: worst(statuses),
|
|
1099
|
+
problems
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
function probeRequest(candidate, key) {
|
|
1103
|
+
const preset = text(candidate.preset);
|
|
1104
|
+
const provider = (text(candidate.provider) ?? "").toLowerCase();
|
|
1105
|
+
const baseUrl = text(candidate.baseUrl) ?? (preset && isInferencePresetId(preset) ? INFERENCE_PRESETS[preset].baseUrl : void 0);
|
|
1106
|
+
const trim = (url) => url.replace(/\/+$/, "");
|
|
1107
|
+
if (!preset && !baseUrl && provider !== "openai" && OPENAI_COMPATIBLE.has(provider)) return;
|
|
1108
|
+
if (preset || OPENAI_COMPATIBLE.has(provider)) return {
|
|
1109
|
+
url: `${trim(baseUrl ?? OPENAI_DEFAULT_BASE)}/models`,
|
|
1110
|
+
headers: key ? { authorization: `Bearer ${key}` } : {}
|
|
1111
|
+
};
|
|
1112
|
+
if (provider === "anthropic") return {
|
|
1113
|
+
url: `${trim(baseUrl ?? ANTHROPIC_DEFAULT_BASE)}/models`,
|
|
1114
|
+
headers: {
|
|
1115
|
+
"anthropic-version": "2023-06-01",
|
|
1116
|
+
...key ? { "x-api-key": key } : {}
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
if (provider === "gemini") return {
|
|
1120
|
+
url: `${trim(baseUrl ?? GEMINI_DEFAULT_BASE)}/models`,
|
|
1121
|
+
headers: key ? { "x-goog-api-key": key } : {}
|
|
1122
|
+
};
|
|
1123
|
+
if (provider === "ollama") return {
|
|
1124
|
+
url: `${trim(baseUrl ?? OLLAMA_DEFAULT_BASE)}/api/tags`,
|
|
1125
|
+
headers: {}
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
function positive(value) {
|
|
1129
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : void 0;
|
|
1130
|
+
}
|
|
1131
|
+
function inferenceCapabilitiesFromModelListing(body, model) {
|
|
1132
|
+
if (!model || !body || typeof body !== "object") return void 0;
|
|
1133
|
+
const record = body;
|
|
1134
|
+
const entry = (Array.isArray(record.data) ? record.data : Array.isArray(record.models) ? record.models : []).find((item) => {
|
|
1135
|
+
const row = item;
|
|
1136
|
+
return row?.id === model || row?.name === model || row?.name === `models/${model}`;
|
|
1137
|
+
});
|
|
1138
|
+
if (!entry) return void 0;
|
|
1139
|
+
const out = {};
|
|
1140
|
+
const meta = entry.meta;
|
|
1141
|
+
const topProvider = entry.top_provider;
|
|
1142
|
+
const maxContext = positive(entry.context_length) ?? positive(entry.max_context_length) ?? positive(topProvider?.context_length) ?? positive(meta?.n_ctx_train) ?? positive(entry.inputTokenLimit);
|
|
1143
|
+
if (maxContext !== void 0) out.maxContext = maxContext;
|
|
1144
|
+
if (Array.isArray(entry.supported_parameters)) {
|
|
1145
|
+
const params = entry.supported_parameters;
|
|
1146
|
+
out.tools = params.includes("tools");
|
|
1147
|
+
out.json = params.includes("response_format") || params.includes("structured_outputs");
|
|
1148
|
+
}
|
|
1149
|
+
const architecture = entry.architecture;
|
|
1150
|
+
if (Array.isArray(architecture?.input_modalities)) out.vision = architecture.input_modalities.includes("image");
|
|
1151
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
1152
|
+
}
|
|
1153
|
+
async function inferenceCandidateCapabilities(candidate, options = {}) {
|
|
1154
|
+
const preset = inferencePresetFor(candidate);
|
|
1155
|
+
let sdk;
|
|
1156
|
+
if (options.sdkCapabilities && !text(candidate.baseUrl) && !preset) try {
|
|
1157
|
+
sdk = await options.sdkCapabilities(candidate);
|
|
1158
|
+
} catch {
|
|
1159
|
+
sdk = void 0;
|
|
1160
|
+
}
|
|
1161
|
+
return resolveInferenceCapabilities({
|
|
1162
|
+
provider: inferenceCapabilitiesFromSdk(sdk),
|
|
1163
|
+
preset: preset ? INFERENCE_PRESET_CAPABILITIES[preset] : void 0,
|
|
1164
|
+
probe: options.probe,
|
|
1165
|
+
heuristic: inferenceCapabilityHeuristic(candidate)
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
function inferenceProbeUrl(candidate) {
|
|
1169
|
+
return probeRequest(candidate, void 0)?.url;
|
|
1170
|
+
}
|
|
1171
|
+
async function probeInferenceCandidate(candidate, options = {}) {
|
|
1172
|
+
const env = options.env ?? process.env;
|
|
1173
|
+
const preset = text(candidate.preset);
|
|
1174
|
+
if (preset && !isInferencePresetId(preset)) return {
|
|
1175
|
+
reachable: false,
|
|
1176
|
+
durationMs: 0,
|
|
1177
|
+
detail: `not probed: unknown preset "${preset}"`
|
|
1178
|
+
};
|
|
1179
|
+
if ((text(candidate.apiKey) || text(candidate.apiKeyEnv)) && !text(candidate.baseUrl) && !preset) return {
|
|
1180
|
+
reachable: false,
|
|
1181
|
+
durationMs: 0,
|
|
1182
|
+
detail: "not probed: a candidate key needs its own baseUrl (it is never sent to a provider default endpoint)"
|
|
1183
|
+
};
|
|
1184
|
+
const request = probeRequest(candidate, resolveKey(candidate, env));
|
|
1185
|
+
if (!request) return {
|
|
1186
|
+
reachable: false,
|
|
1187
|
+
skipped: true,
|
|
1188
|
+
durationMs: 0,
|
|
1189
|
+
detail: `no cheap probe for provider "${candidate.provider ?? "default"}"`
|
|
1190
|
+
};
|
|
1191
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
1192
|
+
const controller = new AbortController();
|
|
1193
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
|
|
1194
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1195
|
+
const started = Date.now();
|
|
1196
|
+
const where = redactBaseUrl(request.url) ?? "endpoint";
|
|
1197
|
+
try {
|
|
1198
|
+
const response = await doFetch(request.url, {
|
|
1199
|
+
method: "GET",
|
|
1200
|
+
headers: request.headers,
|
|
1201
|
+
signal: controller.signal,
|
|
1202
|
+
redirect: "manual"
|
|
1203
|
+
});
|
|
1204
|
+
const durationMs = Date.now() - started;
|
|
1205
|
+
if (response.type === "opaqueredirect" || response.status >= 300 && response.status < 400) return {
|
|
1206
|
+
reachable: false,
|
|
1207
|
+
status: response.status || void 0,
|
|
1208
|
+
durationMs,
|
|
1209
|
+
detail: `${where} redirected (not followed: the key stays with its own endpoint)`
|
|
1210
|
+
};
|
|
1211
|
+
if (response.ok) {
|
|
1212
|
+
let capabilities;
|
|
1213
|
+
try {
|
|
1214
|
+
capabilities = inferenceCapabilitiesFromModelListing(await response.json(), text(candidate.model));
|
|
1215
|
+
} catch {}
|
|
1216
|
+
return {
|
|
1217
|
+
reachable: true,
|
|
1218
|
+
status: response.status,
|
|
1219
|
+
durationMs,
|
|
1220
|
+
detail: `${where} answered ${response.status}`,
|
|
1221
|
+
...capabilities ? { capabilities } : {}
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
const why = response.status === 401 || response.status === 403 ? "credential rejected" : response.status === 402 || response.status === 429 ? "quota or rate limit" : "endpoint error";
|
|
1225
|
+
return {
|
|
1226
|
+
reachable: false,
|
|
1227
|
+
status: response.status,
|
|
1228
|
+
durationMs,
|
|
1229
|
+
detail: `${where} answered ${response.status} (${why})`
|
|
1230
|
+
};
|
|
1231
|
+
} catch (error) {
|
|
1232
|
+
const aborted = controller.signal.aborted;
|
|
1233
|
+
return {
|
|
1234
|
+
reachable: false,
|
|
1235
|
+
durationMs: Date.now() - started,
|
|
1236
|
+
detail: aborted ? `${where} did not answer within ${timeoutMs} ms` : `${where} unreachable (${error instanceof Error ? error.name : "error"})`
|
|
1237
|
+
};
|
|
1238
|
+
} finally {
|
|
1239
|
+
clearTimeout(timer);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
function describeTarget(target) {
|
|
1243
|
+
const kind = inferenceTargetKind(target);
|
|
1244
|
+
if (kind === "alias") return `alias ${String(target.alias)}`;
|
|
1245
|
+
if (kind === "candidates") return `${target.candidates?.length ?? 0} inline candidate(s)`;
|
|
1246
|
+
if (kind === "fusion") return `fusion ${String(target.fusion)}`;
|
|
1247
|
+
if (kind !== "single") return `kind ${kind} (not checked here)`;
|
|
1248
|
+
const base = text(target.baseUrl);
|
|
1249
|
+
return `${candidateLabel(target)}${base ? ` at ${redactBaseUrl(base)}` : ""}`;
|
|
1250
|
+
}
|
|
1251
|
+
async function checkInferenceBindings(options = {}) {
|
|
1252
|
+
const env = options.env ?? process.env;
|
|
1253
|
+
const providers = (options.providers ?? AI_PROVIDER_TYPES).map((name) => name.toLowerCase());
|
|
1254
|
+
const bindings = [];
|
|
1255
|
+
let config;
|
|
1256
|
+
try {
|
|
1257
|
+
config = "config" in options ? options.config : getInferenceConfig();
|
|
1258
|
+
assertInferenceConfig(config);
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1261
|
+
return {
|
|
1262
|
+
ok: false,
|
|
1263
|
+
configured: true,
|
|
1264
|
+
bindings: [{
|
|
1265
|
+
binding: "inference",
|
|
1266
|
+
target: "config",
|
|
1267
|
+
status: "error",
|
|
1268
|
+
problems: [message],
|
|
1269
|
+
candidates: []
|
|
1270
|
+
}],
|
|
1271
|
+
issues: [{
|
|
1272
|
+
path: "inference",
|
|
1273
|
+
code: "candidate-invalid",
|
|
1274
|
+
message
|
|
1275
|
+
}]
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
const issues = findInferenceConfigIssues(config);
|
|
1279
|
+
const aliases = config?.aliases ?? {};
|
|
1280
|
+
const issuesFor = (prefix) => issues.filter((issue) => issue.path === prefix || issue.path.startsWith(`${prefix}.`) || issue.path.startsWith(`${prefix}[`)).map((issue) => issue.message);
|
|
1281
|
+
const checkList = async (list) => {
|
|
1282
|
+
const out = [];
|
|
1283
|
+
for (const candidate of list) {
|
|
1284
|
+
const nested = text(candidate.alias);
|
|
1285
|
+
if (nested) {
|
|
1286
|
+
out.push({
|
|
1287
|
+
label: `alias:${nested}`,
|
|
1288
|
+
status: Object.hasOwn(aliases, nested) ? "ok" : "error",
|
|
1289
|
+
problems: Object.hasOwn(aliases, nested) ? [`expands alias ${nested} (checked under alias:${nested})`] : [`unknown alias "${nested}"`]
|
|
1290
|
+
});
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
const checked = checkCandidateOffline(candidate, {
|
|
1294
|
+
env,
|
|
1295
|
+
providers
|
|
1296
|
+
});
|
|
1297
|
+
if (options.probe && checked.status !== "error") {
|
|
1298
|
+
checked.probe = await probeInferenceCandidate(candidate, {
|
|
1299
|
+
env,
|
|
1300
|
+
timeoutMs: options.timeoutMs,
|
|
1301
|
+
fetch: options.fetch
|
|
1302
|
+
});
|
|
1303
|
+
if (!checked.probe.reachable && !checked.probe.skipped) {
|
|
1304
|
+
checked.status = "error";
|
|
1305
|
+
checked.problems.push(checked.probe.detail);
|
|
1306
|
+
}
|
|
1307
|
+
checked.capabilities = await inferenceCandidateCapabilities(candidate, {
|
|
1308
|
+
probe: checked.probe.capabilities,
|
|
1309
|
+
sdkCapabilities: options.sdkCapabilities
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
out.push(checked);
|
|
1313
|
+
}
|
|
1314
|
+
return out;
|
|
1315
|
+
};
|
|
1316
|
+
const settle = (binding, target, configProblems, candidates) => {
|
|
1317
|
+
const usable = candidates.some((candidate) => candidate.status !== "error");
|
|
1318
|
+
return {
|
|
1319
|
+
binding,
|
|
1320
|
+
target,
|
|
1321
|
+
status: configProblems.length > 0 || candidates.length > 0 && !usable ? "error" : candidates.every((candidate) => candidate.status === "ok") ? "ok" : "warn",
|
|
1322
|
+
problems: [...configProblems, ...configProblems.length === 0 && candidates.length > 0 && !usable ? ["no candidate can answer"] : []],
|
|
1323
|
+
candidates
|
|
1324
|
+
};
|
|
1325
|
+
};
|
|
1326
|
+
for (const tier of INFERENCE_TIERS) {
|
|
1327
|
+
const target = config?.tiers?.[tier];
|
|
1328
|
+
const name = `tier:${tier}`;
|
|
1329
|
+
if (!target || Object.keys(target).length === 0) {
|
|
1330
|
+
bindings.push({
|
|
1331
|
+
binding: name,
|
|
1332
|
+
target: "the configured client",
|
|
1333
|
+
status: "ok",
|
|
1334
|
+
problems: [],
|
|
1335
|
+
candidates: []
|
|
1336
|
+
});
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
const kind = inferenceTargetKind(target);
|
|
1340
|
+
const configProblems = issuesFor(`inference.tiers.${tier}`);
|
|
1341
|
+
let candidates = [];
|
|
1342
|
+
if (kind === "single") candidates = await checkList([target]);
|
|
1343
|
+
else if (kind === "candidates") candidates = await checkList(target.candidates ?? []);
|
|
1344
|
+
else if (kind === "alias") {
|
|
1345
|
+
const aliasName = text(target.alias);
|
|
1346
|
+
candidates = [{
|
|
1347
|
+
label: `alias:${aliasName ?? "?"}`,
|
|
1348
|
+
status: aliasName && Object.hasOwn(aliases, aliasName) ? "ok" : "error",
|
|
1349
|
+
problems: []
|
|
1350
|
+
}];
|
|
1351
|
+
}
|
|
1352
|
+
bindings.push(settle(name, describeTarget(target), configProblems, candidates));
|
|
1353
|
+
}
|
|
1354
|
+
for (const [name, alias] of Object.entries(aliases)) {
|
|
1355
|
+
const list = Array.isArray(alias?.candidates) ? alias.candidates : [];
|
|
1356
|
+
bindings.push(settle(`alias:${name}`, `${list.length} candidate(s)${alias?.description ? ` \u2014 ${alias.description}` : ""}`, issuesFor(`inference.aliases.${name}`), await checkList(list)));
|
|
1357
|
+
}
|
|
1358
|
+
for (const [name, fusion] of Object.entries(config?.fusions ?? {})) {
|
|
1359
|
+
const seats = Array.isArray(fusion?.seats) ? fusion.seats.length : 0;
|
|
1360
|
+
bindings.push(settle(`fusion:${name}`, `${String(fusion?.mode)}, ${seats} seat(s)${fusion?.description ? ` \u2014 ${fusion.description}` : ""}`, issuesFor(`inference.fusions.${name}`), []));
|
|
1361
|
+
}
|
|
1362
|
+
for (const binding of bindings) {
|
|
1363
|
+
if (!binding.binding.startsWith("tier:")) continue;
|
|
1364
|
+
const ref = binding.candidates[0]?.label;
|
|
1365
|
+
if (binding.candidates.length !== 1 || !ref?.startsWith("alias:")) continue;
|
|
1366
|
+
const resolved = bindings.find((entry) => entry.binding === ref);
|
|
1367
|
+
const reference = binding.candidates[0];
|
|
1368
|
+
if (resolved && reference) reference.status = resolved.status;
|
|
1369
|
+
if (resolved?.status === "error") {
|
|
1370
|
+
binding.status = "error";
|
|
1371
|
+
binding.problems.push(`${ref} cannot resolve`);
|
|
1372
|
+
} else if (resolved?.status === "warn" && binding.status === "ok") {
|
|
1373
|
+
binding.status = "warn";
|
|
1374
|
+
binding.problems.push(`${ref} has a candidate that cannot answer`);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
return {
|
|
1378
|
+
ok: !bindings.some((binding) => binding.status === "error"),
|
|
1379
|
+
configured: !!config && (!!config.tiers || !!config.aliases),
|
|
1380
|
+
bindings,
|
|
1381
|
+
issues
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
function formatInferenceCapabilities(capabilities) {
|
|
1385
|
+
const flag = (value) => value ? "yes" : "no";
|
|
1386
|
+
return `json ${flag(capabilities.json)}, tools ${flag(capabilities.tools)}, vision ${flag(capabilities.vision)}, context ${capabilities.maxContext ?? "unknown"} (source: ${capabilities.source})`;
|
|
1387
|
+
}
|
|
1388
|
+
var ICON = {
|
|
1389
|
+
ok: "✅",
|
|
1390
|
+
warn: "⚠️ ",
|
|
1391
|
+
error: "❌"
|
|
1392
|
+
};
|
|
1393
|
+
function formatInferenceDoctorReport(report) {
|
|
1394
|
+
const lines = [];
|
|
1395
|
+
if (!report.configured) lines.push(" No inference config: every tier uses the configured AI client.");
|
|
1396
|
+
for (const binding of report.bindings) {
|
|
1397
|
+
lines.push(` ${ICON[binding.status]} ${binding.binding} \u2192 ${binding.target}`);
|
|
1398
|
+
for (const problem of binding.problems) lines.push(` ${problem}`);
|
|
1399
|
+
for (const candidate of binding.candidates) {
|
|
1400
|
+
const probe = candidate.probe ? candidate.probe.skipped ? " (probe skipped)" : ` (${candidate.probe.detail}, ${candidate.probe.durationMs} ms)` : "";
|
|
1401
|
+
lines.push(` ${ICON[candidate.status]} ${candidate.label}${probe}`);
|
|
1402
|
+
if (candidate.capabilities) lines.push(` capabilities: ${formatInferenceCapabilities(candidate.capabilities)}`);
|
|
1403
|
+
for (const problem of candidate.problems) {
|
|
1404
|
+
if (candidate.probe && problem === candidate.probe.detail) continue;
|
|
1405
|
+
lines.push(` ${problem}`);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
return lines;
|
|
1410
|
+
}
|
|
1411
|
+
//#endregion
|
|
1412
|
+
export { HOSTED_BUDGET_UNCERTAIN_WINDOW_MS, HOSTED_BUDGET_WINDOWS, HOSTED_GATEWAY_ADMIN_METHODS, HOSTED_GATEWAY_ADMIN_TIMEOUT_MS, HOSTED_GATEWAY_ENV, HOSTED_GATEWAY_PROVIDERS, HOSTED_INFERENCE_FEATURE_KEY, HOSTED_INFERENCE_SECRET_CATEGORY, HostedInferenceError, checkInferenceBindings, createHostedGatewayAdmin, createHostedInferenceEntitlementListener, createHostedInferenceProvisioner, createHostedInferenceReconciler, formatInferenceCapabilities, formatInferenceDoctorReport, getHostedGatewayConfig, hostedBudgetFromGrantMetadata, hostedGatewayConfigFromEnv, hostedInferenceStateSecretName, inferenceCandidateCapabilities, inferenceCapabilitiesFromModelListing, inferenceProbeUrl, normalizeHostedBudget, parseHostedGatewayConfig, probeInferenceCandidate, provisionHostedInference, resetHostedGatewayConfig, revokeHostedInference, toGatewayBudget };
|
|
1413
|
+
|
|
1414
|
+
//# sourceMappingURL=node.js.map
|