@happyvertical/smrt-inference 0.0.0-stage → 0.60.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +201 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +73 -2
- package/agents/fusions.md +92 -0
- package/agents/hosted.md +145 -0
- package/agents/resolver.md +65 -0
- package/agents/settings.md +84 -0
- package/dist/__smrt-register__.d.ts +2 -0
- package/dist/__smrt-register__.d.ts.map +1 -0
- package/dist/chunks/InferenceOverrideCollection-wl7sI-3C.js +398 -0
- package/dist/chunks/InferenceOverrideCollection-wl7sI-3C.js.map +1 -0
- package/dist/chunks/provider-config-4Bx8N52X.js +1847 -0
- package/dist/chunks/provider-config-4Bx8N52X.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 +454 -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 +1361 -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 +288 -0
- package/dist/resolver.js.map +1 -0
- package/dist/server.d.ts +24 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +985 -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 +54 -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 +34 -0
- package/dist/settings/resolver.d.ts.map +1 -0
- package/dist/settings/settings-service.d.ts +282 -0
- package/dist/settings/settings-service.d.ts.map +1 -0
- package/dist/settings/targets.d.ts +93 -0
- package/dist/settings/targets.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +335 -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 +135 -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 +345 -0
- package/dist/svelte/components/InferenceSettingsPanel.svelte.d.ts +65 -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/InferenceUsageTable.svelte +127 -0
- package/dist/svelte/components/InferenceUsageTable.svelte.d.ts +15 -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 +197 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +96 -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
|
@@ -0,0 +1,1847 @@
|
|
|
1
|
+
import { INFERENCE_FUSION_MODES, INFERENCE_MODES, INFERENCE_PRESETS, INFERENCE_TIERS, InferenceUnavailableError, builtInInferenceResolver, compareInferenceTiers, getInferenceExecutor, getInferenceResolver, importOptionalDependency, inferenceTargetKind, isInferenceMode, isInferencePresetId, isInferenceTier, isInferenceTierRequest, isInferenceUnavailableError, resolveAutoTier, setInferenceExecutor, setInferenceResolver } from "@happyvertical/smrt-core";
|
|
2
|
+
import { SmrtError } from "@happyvertical/smrt-core/smrt-error";
|
|
3
|
+
//#region src/errors.ts
|
|
4
|
+
var InferenceConfigError = class extends SmrtError {
|
|
5
|
+
issues;
|
|
6
|
+
constructor(issues) {
|
|
7
|
+
super(`Invalid inference config: ${issues.map((issue) => issue.message).join("; ")}`, "INFERENCE_CONFIG_INVALID", "configuration", { issues });
|
|
8
|
+
this.issues = issues;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var InferenceExhaustedError = class extends SmrtError {
|
|
12
|
+
/** The failure that ended the walk. */
|
|
13
|
+
reason;
|
|
14
|
+
/** The full fallback trail, ending with `to: null`. */
|
|
15
|
+
substitutions;
|
|
16
|
+
/** Text already streamed to the caller, for a `partial` ending. */
|
|
17
|
+
partialText;
|
|
18
|
+
constructor(input) {
|
|
19
|
+
const last = input.substitutions.at(-1);
|
|
20
|
+
super(input.reason === "partial" ? `Inference candidate ${last?.from ?? "unknown"} failed after streaming; no other candidate may extend its reply.` : `No inference candidate answered tier "${input.tierRequested}" (last failure: ${input.reason}).`, "INFERENCE_EXHAUSTED", "ai", {
|
|
21
|
+
reason: input.reason,
|
|
22
|
+
tierRequested: input.tierRequested,
|
|
23
|
+
substitutions: input.substitutions
|
|
24
|
+
}, input.cause instanceof Error ? input.cause : void 0);
|
|
25
|
+
this.reason = input.reason;
|
|
26
|
+
this.substitutions = input.substitutions;
|
|
27
|
+
if (input.partialText !== void 0) this.partialText = input.partialText;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var InferenceFusionError = class extends SmrtError {
|
|
31
|
+
result;
|
|
32
|
+
constructor(result, cause) {
|
|
33
|
+
const failures = result.seats.filter((seat) => seat.failure).map((seat) => `${seat.name}: ${seat.failure?.reason}`);
|
|
34
|
+
super(`Fusion "${result.fusionKey}" produced no answer (run ${result.runId}${failures.length > 0 ? `; ${failures.join(", ")}` : ""}).`, "INFERENCE_FUSION_FAILED", "ai", {
|
|
35
|
+
runId: result.runId,
|
|
36
|
+
fusionKey: result.fusionKey,
|
|
37
|
+
failures
|
|
38
|
+
}, cause instanceof Error ? cause : void 0);
|
|
39
|
+
this.result = result;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/fusion-store.ts
|
|
44
|
+
var STORE_KEY = /* @__PURE__ */ Symbol.for("smrt.inference.fusions");
|
|
45
|
+
function fusionStore() {
|
|
46
|
+
const holder = globalThis;
|
|
47
|
+
holder[STORE_KEY] ??= /* @__PURE__ */ new Map();
|
|
48
|
+
return holder[STORE_KEY];
|
|
49
|
+
}
|
|
50
|
+
function lookupFusion(key, config) {
|
|
51
|
+
const fromConfig = config?.fusions;
|
|
52
|
+
if (fromConfig && Object.hasOwn(fromConfig, key)) return fromConfig[key];
|
|
53
|
+
return fusionStore().get(key);
|
|
54
|
+
}
|
|
55
|
+
function isPromptRegistered(key) {
|
|
56
|
+
const registry = globalThis.__smrtPromptRegistry;
|
|
57
|
+
return registry instanceof Map && registry.has(key);
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/config.ts
|
|
61
|
+
var CHAIN_KINDS = ["alias", "candidates"];
|
|
62
|
+
var KEYLESS_PLACEHOLDER$1 = "smrt-local";
|
|
63
|
+
function isRecord$1(value) {
|
|
64
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
65
|
+
}
|
|
66
|
+
function text$1(value) {
|
|
67
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
68
|
+
}
|
|
69
|
+
var EXECUTOR_KINDS = [...CHAIN_KINDS, "fusion"];
|
|
70
|
+
function hasInferenceChains(config) {
|
|
71
|
+
if (!config) return false;
|
|
72
|
+
if (isRecord$1(config.aliases) && Object.keys(config.aliases).length > 0) return true;
|
|
73
|
+
if (isRecord$1(config.fusions) && Object.keys(config.fusions).length > 0) return true;
|
|
74
|
+
return Object.values(config.tiers ?? {}).some((target) => EXECUTOR_KINDS.includes(inferenceTargetKind(target)));
|
|
75
|
+
}
|
|
76
|
+
function checkCandidate(candidate, path, aliases, issues) {
|
|
77
|
+
if (!isRecord$1(candidate)) {
|
|
78
|
+
issues.push({
|
|
79
|
+
path,
|
|
80
|
+
code: "candidate-invalid",
|
|
81
|
+
message: `${path} must be an object`
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const alias = text$1(candidate.alias);
|
|
86
|
+
if (candidate.alias !== void 0) {
|
|
87
|
+
if (!alias) issues.push({
|
|
88
|
+
path,
|
|
89
|
+
code: "candidate-invalid",
|
|
90
|
+
message: `${path}.alias must be a non-empty string`
|
|
91
|
+
});
|
|
92
|
+
else if (!Object.hasOwn(aliases, alias)) issues.push({
|
|
93
|
+
path,
|
|
94
|
+
code: "alias-unknown",
|
|
95
|
+
message: `${path} references unknown alias "${alias}"`
|
|
96
|
+
});
|
|
97
|
+
const extra = [
|
|
98
|
+
"provider",
|
|
99
|
+
"model",
|
|
100
|
+
"preset",
|
|
101
|
+
"baseUrl",
|
|
102
|
+
"apiKeyEnv"
|
|
103
|
+
].find((key) => candidate[key] !== void 0);
|
|
104
|
+
if (extra) issues.push({
|
|
105
|
+
path,
|
|
106
|
+
code: "candidate-invalid",
|
|
107
|
+
message: `${path} names an alias and "${extra}"; an alias candidate takes nothing else`
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const preset = text$1(candidate.preset);
|
|
112
|
+
if (candidate.preset !== void 0) {
|
|
113
|
+
if (!preset || !isInferencePresetId(preset)) issues.push({
|
|
114
|
+
path,
|
|
115
|
+
code: "preset-unknown",
|
|
116
|
+
message: `${path} names unknown preset "${String(candidate.preset)}"; expected one of ${Object.keys(INFERENCE_PRESETS).join(", ")}`
|
|
117
|
+
});
|
|
118
|
+
if (!text$1(candidate.model)) issues.push({
|
|
119
|
+
path,
|
|
120
|
+
code: "candidate-invalid",
|
|
121
|
+
message: `${path} names preset "${String(candidate.preset)}" without a model`
|
|
122
|
+
});
|
|
123
|
+
if (candidate.provider !== void 0) issues.push({
|
|
124
|
+
path,
|
|
125
|
+
code: "candidate-invalid",
|
|
126
|
+
message: `${path} names both a preset and a provider`
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!text$1(candidate.provider) && !text$1(candidate.model)) {
|
|
131
|
+
issues.push({
|
|
132
|
+
path,
|
|
133
|
+
code: "candidate-invalid",
|
|
134
|
+
message: `${path} needs an alias, a preset, or a provider and model`
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if ((text$1(candidate.apiKeyEnv) || text$1(candidate.apiKey)) && !text$1(candidate.baseUrl)) issues.push({
|
|
139
|
+
path,
|
|
140
|
+
code: "candidate-key-without-endpoint",
|
|
141
|
+
message: `${path} brings its own key without a baseUrl; a key travels only with its own endpoint (name the provider's baseUrl, or drop apiKeyEnv to use the provider's own key variable)`
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function checkCandidateList(list, path, aliases, issues) {
|
|
145
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
146
|
+
issues.push({
|
|
147
|
+
path,
|
|
148
|
+
code: "alias-empty",
|
|
149
|
+
message: `${path} has no candidates`
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
list.forEach((candidate, index) => {
|
|
154
|
+
checkCandidate(candidate, `${path}[${index}]`, aliases, issues);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function checkTarget(target, path, aliases, issues, config) {
|
|
158
|
+
const kind = inferenceTargetKind(target);
|
|
159
|
+
if (kind === "alias") {
|
|
160
|
+
const name = text$1(target.alias);
|
|
161
|
+
if (!name) issues.push({
|
|
162
|
+
path,
|
|
163
|
+
code: "candidate-invalid",
|
|
164
|
+
message: `${path} has kind "alias" without an alias name`
|
|
165
|
+
});
|
|
166
|
+
else if (!Object.hasOwn(aliases, name)) issues.push({
|
|
167
|
+
path,
|
|
168
|
+
code: "alias-unknown",
|
|
169
|
+
message: `${path} points at unknown alias "${name}"`
|
|
170
|
+
});
|
|
171
|
+
} else if (kind === "candidates") checkCandidateList(target.candidates, `${path}.candidates`, aliases, issues);
|
|
172
|
+
else if (kind === "fusion") {
|
|
173
|
+
const name = text$1(target.fusion);
|
|
174
|
+
if (!name) issues.push({
|
|
175
|
+
path,
|
|
176
|
+
code: "fusion-invalid",
|
|
177
|
+
message: `${path} has kind "fusion" without a fusion key`
|
|
178
|
+
});
|
|
179
|
+
else if (!lookupFusion(name, config)) issues.push({
|
|
180
|
+
path,
|
|
181
|
+
code: "fusion-unknown",
|
|
182
|
+
message: `${path} points at unknown fusion "${name}" (define it with defineFusion() or inference.fusions)`
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
var FUSION_SEAT_COUNTS = {
|
|
187
|
+
single: 1,
|
|
188
|
+
pair: 3,
|
|
189
|
+
handoff: 2
|
|
190
|
+
};
|
|
191
|
+
var FUSION_GATE_CONTINUE = "continue";
|
|
192
|
+
var FUSION_GATE_SEAT = "gate";
|
|
193
|
+
function findFusionDefinitionIssues(definition, path, config) {
|
|
194
|
+
const issues = [];
|
|
195
|
+
const invalid = (at, message) => issues.push({
|
|
196
|
+
path: at,
|
|
197
|
+
code: "fusion-invalid",
|
|
198
|
+
message
|
|
199
|
+
});
|
|
200
|
+
if (!isRecord$1(definition)) {
|
|
201
|
+
invalid(path, `${path} must be an object with mode and seats`);
|
|
202
|
+
return issues;
|
|
203
|
+
}
|
|
204
|
+
const mode = definition.mode;
|
|
205
|
+
const known = INFERENCE_FUSION_MODES.includes(mode);
|
|
206
|
+
if (!known) invalid(`${path}.mode`, `${path}.mode must be one of ${INFERENCE_FUSION_MODES.join(", ")}; got "${String(mode)}"`);
|
|
207
|
+
if (definition.description !== void 0 && typeof definition.description !== "string") invalid(`${path}.description`, `${path}.description must be a string`);
|
|
208
|
+
const seats = definition.seats;
|
|
209
|
+
if (!Array.isArray(seats)) invalid(`${path}.seats`, `${path}.seats must be an array of seats`);
|
|
210
|
+
else {
|
|
211
|
+
const expected = known ? FUSION_SEAT_COUNTS[mode] : void 0;
|
|
212
|
+
if (expected !== void 0 && seats.length !== expected) invalid(`${path}.seats`, `${path}.seats must hold exactly ${expected} seat(s) for mode "${String(mode)}"; got ${seats.length}`);
|
|
213
|
+
const aliases = isRecord$1(config?.aliases) ? config.aliases : {};
|
|
214
|
+
const names = /* @__PURE__ */ new Map();
|
|
215
|
+
seats.forEach((seat, index) => {
|
|
216
|
+
const at = `${path}.seats[${index}]`;
|
|
217
|
+
if (!isRecord$1(seat)) {
|
|
218
|
+
invalid(at, `${at} must be an object with prompt and target`);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const prompt = text$1(seat.prompt);
|
|
222
|
+
if (!prompt) invalid(`${at}.prompt`, `${at}.prompt must name a registered prompt key`);
|
|
223
|
+
if (seat.name !== void 0 && !text$1(seat.name)) invalid(`${at}.name`, `${at}.name must be a non-empty string`);
|
|
224
|
+
const name = text$1(seat.name) ?? prompt;
|
|
225
|
+
if (name) {
|
|
226
|
+
const first = names.get(name);
|
|
227
|
+
if (name === "gate") invalid(`${at}.name`, `${at}.name "${FUSION_GATE_SEAT}" is reserved for the decision gate's usage row`);
|
|
228
|
+
else if (first !== void 0) invalid(`${at}.name`, `${at} has the same name "${name}" as seats[${first}]; name each seat (the name defaults to the prompt key)`);
|
|
229
|
+
else names.set(name, index);
|
|
230
|
+
}
|
|
231
|
+
if (seat.temperature !== void 0 && (typeof seat.temperature !== "number" || !Number.isFinite(seat.temperature))) invalid(`${at}.temperature`, `${at}.temperature must be a number`);
|
|
232
|
+
if (seat.output !== void 0 && seat.output !== "text" && seat.output !== "json") invalid(`${at}.output`, `${at}.output must be "text" or "json"`);
|
|
233
|
+
const target = seat.target;
|
|
234
|
+
if (typeof target === "string") {
|
|
235
|
+
if (!isInferenceTierRequest(target)) invalid(`${at}.target`, `${at}.target "${target}" is not a tier (auto, ${INFERENCE_TIERS.join(", ")})`);
|
|
236
|
+
} else if (isRecord$1(target)) if (inferenceTargetKind(target) === "fusion") issues.push({
|
|
237
|
+
path: `${at}.target`,
|
|
238
|
+
code: "fusion-nested",
|
|
239
|
+
message: `${at}.target names another fusion; seats run a tier, an alias or a model, never a fusion`
|
|
240
|
+
});
|
|
241
|
+
else checkTarget(target, `${at}.target`, aliases, issues, config);
|
|
242
|
+
else invalid(`${at}.target`, `${at}.target must be a tier name or a target ({ alias }, { candidates } or { provider, model })`);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
const gate = definition.gate;
|
|
246
|
+
if (gate !== void 0) if (mode !== "pair" && mode !== "handoff") invalid(`${path}.gate`, `${path}.gate applies to pair and handoff only`);
|
|
247
|
+
else if (!isRecord$1(gate)) invalid(`${path}.gate`, `${path}.gate must be an object`);
|
|
248
|
+
else {
|
|
249
|
+
if (!text$1(gate.criteria)) invalid(`${path}.gate.criteria`, `${path}.gate.criteria must be a non-empty string`);
|
|
250
|
+
const when = gate.sufficientWhen;
|
|
251
|
+
if (!isRecord$1(when)) invalid(`${path}.gate.sufficientWhen`, `${path}.gate.sufficientWhen must be { choice, minConfidence }`);
|
|
252
|
+
else {
|
|
253
|
+
const choice = text$1(when.choice);
|
|
254
|
+
if (!choice || choice === "continue") invalid(`${path}.gate.sufficientWhen.choice`, `${path}.gate.sufficientWhen.choice must be a non-empty label other than "${FUSION_GATE_CONTINUE}"`);
|
|
255
|
+
const min = when.minConfidence;
|
|
256
|
+
if (typeof min !== "number" || !Number.isFinite(min) || min < 0 || min > 1) invalid(`${path}.gate.sufficientWhen.minConfidence`, `${path}.gate.sufficientWhen.minConfidence must be a number in [0, 1]`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return issues;
|
|
260
|
+
}
|
|
261
|
+
function findFusionDefinitionPromptIssues(definition, path) {
|
|
262
|
+
if (!isRecord$1(definition) || !Array.isArray(definition.seats)) return [];
|
|
263
|
+
const issues = [];
|
|
264
|
+
definition.seats.forEach((seat, index) => {
|
|
265
|
+
const prompt = isRecord$1(seat) ? text$1(seat.prompt) : void 0;
|
|
266
|
+
if (prompt && !isPromptRegistered(prompt)) {
|
|
267
|
+
const at = `${path}.seats[${index}].prompt`;
|
|
268
|
+
issues.push({
|
|
269
|
+
path: at,
|
|
270
|
+
code: "fusion-prompt-unknown",
|
|
271
|
+
message: `${at} names prompt "${prompt}", which no definePrompt() registered`
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
return issues;
|
|
276
|
+
}
|
|
277
|
+
function findFusionPromptIssues(config) {
|
|
278
|
+
if (!isRecord$1(config?.fusions)) return [];
|
|
279
|
+
return Object.entries(config.fusions).flatMap(([key, definition]) => findFusionDefinitionPromptIssues(definition, `inference.fusions.${key}`));
|
|
280
|
+
}
|
|
281
|
+
function validateInferenceTarget(target, path, config) {
|
|
282
|
+
const issues = [];
|
|
283
|
+
checkTarget(target, path, isRecord$1(config?.aliases) ? config.aliases : {}, issues, config);
|
|
284
|
+
if (issues.length > 0) throw new InferenceConfigError(issues);
|
|
285
|
+
}
|
|
286
|
+
function aliasEdges(alias) {
|
|
287
|
+
if (!isRecord$1(alias) || !Array.isArray(alias.candidates)) return [];
|
|
288
|
+
return alias.candidates.map((candidate) => isRecord$1(candidate) ? text$1(candidate.alias) : void 0).filter((name) => !!name);
|
|
289
|
+
}
|
|
290
|
+
function findInferenceConfigIssues(config) {
|
|
291
|
+
const issues = [];
|
|
292
|
+
if (!config) return issues;
|
|
293
|
+
const aliases = isRecord$1(config.aliases) ? config.aliases : {};
|
|
294
|
+
for (const [name, alias] of Object.entries(aliases)) {
|
|
295
|
+
const path = `inference.aliases.${name}`;
|
|
296
|
+
if (!isRecord$1(alias)) {
|
|
297
|
+
issues.push({
|
|
298
|
+
path,
|
|
299
|
+
code: "alias-empty",
|
|
300
|
+
message: `${path} must be an object with candidates`
|
|
301
|
+
});
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
checkCandidateList(alias.candidates, `${path}.candidates`, aliases, issues);
|
|
305
|
+
}
|
|
306
|
+
for (const tier of INFERENCE_TIERS) {
|
|
307
|
+
const target = config.tiers?.[tier];
|
|
308
|
+
if (!target) continue;
|
|
309
|
+
checkTarget(target, `inference.tiers.${tier}`, aliases, issues, config);
|
|
310
|
+
}
|
|
311
|
+
if (config.fusions !== void 0 && !isRecord$1(config.fusions)) issues.push({
|
|
312
|
+
path: "inference.fusions",
|
|
313
|
+
code: "fusion-invalid",
|
|
314
|
+
message: "inference.fusions must be an object of named fusions"
|
|
315
|
+
});
|
|
316
|
+
else for (const [key, definition] of Object.entries(config.fusions ?? {})) issues.push(...findFusionDefinitionIssues(definition, `inference.fusions.${key}`, config));
|
|
317
|
+
const state = /* @__PURE__ */ new Map();
|
|
318
|
+
const reported = /* @__PURE__ */ new Set();
|
|
319
|
+
const visit = (name, stack) => {
|
|
320
|
+
if (state.get(name) === "done") return;
|
|
321
|
+
if (state.get(name) === "visiting") {
|
|
322
|
+
const cycle = [...stack.slice(stack.indexOf(name)), name];
|
|
323
|
+
const key = [...cycle.slice(0, -1)].sort().join("|");
|
|
324
|
+
if (!reported.has(key)) {
|
|
325
|
+
reported.add(key);
|
|
326
|
+
issues.push({
|
|
327
|
+
path: `inference.aliases.${name}`,
|
|
328
|
+
code: "alias-cycle",
|
|
329
|
+
message: `inference.aliases has a cycle: ${cycle.join(" -> ")}`
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
state.set(name, "visiting");
|
|
335
|
+
for (const next of aliasEdges(aliases[name])) if (Object.hasOwn(aliases, next)) visit(next, [...stack, name]);
|
|
336
|
+
state.set(name, "done");
|
|
337
|
+
};
|
|
338
|
+
for (const name of Object.keys(aliases)) visit(name, []);
|
|
339
|
+
return issues;
|
|
340
|
+
}
|
|
341
|
+
function validateInferenceConfig(config) {
|
|
342
|
+
const issues = [...findInferenceConfigIssues(config), ...findFusionPromptIssues(config)];
|
|
343
|
+
if (issues.length > 0) throw new InferenceConfigError(issues);
|
|
344
|
+
}
|
|
345
|
+
function readEnv(name) {
|
|
346
|
+
const env = globalThis.process?.env;
|
|
347
|
+
return text$1(env?.[name]);
|
|
348
|
+
}
|
|
349
|
+
function candidateLabel(candidate) {
|
|
350
|
+
const model = text$1(candidate.model);
|
|
351
|
+
const preset = text$1(candidate.preset);
|
|
352
|
+
if (preset) return `preset:${preset}/${model ?? "default"}`;
|
|
353
|
+
const provider = text$1(candidate.provider);
|
|
354
|
+
if (!provider && !model) return "default";
|
|
355
|
+
return `${provider ?? "default"}/${model ?? "default"}`;
|
|
356
|
+
}
|
|
357
|
+
function candidateToStep(candidate, tier, alias) {
|
|
358
|
+
const label = candidateLabel(candidate);
|
|
359
|
+
const model = text$1(candidate.model);
|
|
360
|
+
const params = isRecord$1(candidate.params) ? candidate.params : void 0;
|
|
361
|
+
const keyEnv = text$1(candidate.apiKeyEnv);
|
|
362
|
+
const key = text$1(candidate.apiKey) ?? (keyEnv ? readEnv(keyEnv) : void 0);
|
|
363
|
+
const missingKey = !!keyEnv && !key && !text$1(candidate.apiKey);
|
|
364
|
+
const preset = text$1(candidate.preset);
|
|
365
|
+
let target;
|
|
366
|
+
if (preset && isInferencePresetId(preset)) {
|
|
367
|
+
const definition = INFERENCE_PRESETS[preset];
|
|
368
|
+
target = {
|
|
369
|
+
provider: "openai",
|
|
370
|
+
baseUrl: text$1(candidate.baseUrl) ?? definition.baseUrl,
|
|
371
|
+
...model ? { model } : {},
|
|
372
|
+
apiKey: key ?? KEYLESS_PLACEHOLDER$1
|
|
373
|
+
};
|
|
374
|
+
const presetNeedsKey = definition.requiresApiKey && !key;
|
|
375
|
+
return {
|
|
376
|
+
tier,
|
|
377
|
+
label,
|
|
378
|
+
target,
|
|
379
|
+
...params ? { params } : {},
|
|
380
|
+
identity: JSON.stringify([
|
|
381
|
+
"preset",
|
|
382
|
+
preset,
|
|
383
|
+
model,
|
|
384
|
+
target.baseUrl,
|
|
385
|
+
key
|
|
386
|
+
]),
|
|
387
|
+
...missingKey || presetNeedsKey ? { unavailable: "credential" } : {},
|
|
388
|
+
...alias ? { alias } : {}
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
const provider = text$1(candidate.provider);
|
|
392
|
+
const baseUrl = text$1(candidate.baseUrl);
|
|
393
|
+
target = {
|
|
394
|
+
...provider ? { provider } : {},
|
|
395
|
+
...model ? { model } : {},
|
|
396
|
+
...baseUrl ? { baseUrl } : {},
|
|
397
|
+
...key ? { apiKey: key } : baseUrl ? { apiKey: KEYLESS_PLACEHOLDER$1 } : {}
|
|
398
|
+
};
|
|
399
|
+
return {
|
|
400
|
+
tier,
|
|
401
|
+
label,
|
|
402
|
+
target,
|
|
403
|
+
...params ? { params } : {},
|
|
404
|
+
identity: JSON.stringify([
|
|
405
|
+
provider,
|
|
406
|
+
model,
|
|
407
|
+
baseUrl,
|
|
408
|
+
key ?? keyEnv
|
|
409
|
+
]),
|
|
410
|
+
...missingKey ? { unavailable: "credential" } : {},
|
|
411
|
+
...alias ? { alias } : {}
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function expandCandidates(candidates, tier, aliases, owner, visiting, out) {
|
|
415
|
+
for (const candidate of candidates) {
|
|
416
|
+
const nested = text$1(candidate.alias);
|
|
417
|
+
if (nested) expandAlias(nested, tier, aliases, visiting, out);
|
|
418
|
+
else out.push(candidateToStep(candidate, tier, owner));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function expandAlias(name, tier, aliases, visiting, out) {
|
|
422
|
+
if (visiting.has(name)) throw new InferenceConfigError([{
|
|
423
|
+
path: `inference.aliases.${name}`,
|
|
424
|
+
code: "alias-cycle",
|
|
425
|
+
message: `inference.aliases has a cycle through "${name}"`
|
|
426
|
+
}]);
|
|
427
|
+
const alias = aliases[name];
|
|
428
|
+
if (!alias || !Array.isArray(alias.candidates)) throw new InferenceConfigError([{
|
|
429
|
+
path: `inference.aliases.${name}`,
|
|
430
|
+
code: "alias-unknown",
|
|
431
|
+
message: `unknown alias "${name}"`
|
|
432
|
+
}]);
|
|
433
|
+
visiting.add(name);
|
|
434
|
+
expandCandidates(alias.candidates, tier, aliases, name, visiting, out);
|
|
435
|
+
visiting.delete(name);
|
|
436
|
+
}
|
|
437
|
+
function expandTarget(target, tier, config) {
|
|
438
|
+
const aliases = config?.aliases ?? {};
|
|
439
|
+
const kind = inferenceTargetKind(target);
|
|
440
|
+
const out = [];
|
|
441
|
+
if (kind === "alias") expandAlias(String(target.alias ?? ""), tier, aliases, /* @__PURE__ */ new Set(), out);
|
|
442
|
+
else if (kind === "candidates") expandCandidates(target.candidates ?? [], tier, aliases, void 0, /* @__PURE__ */ new Set(), out);
|
|
443
|
+
else if (kind === "single") {
|
|
444
|
+
const { kind: _kind, latency: _latency, params, ...single } = target;
|
|
445
|
+
const provider = text$1(single.provider);
|
|
446
|
+
const model = text$1(single.model);
|
|
447
|
+
out.push({
|
|
448
|
+
tier,
|
|
449
|
+
label: candidateLabel({
|
|
450
|
+
provider,
|
|
451
|
+
model
|
|
452
|
+
}),
|
|
453
|
+
target: { ...single },
|
|
454
|
+
...isRecord$1(params) ? { params } : {},
|
|
455
|
+
identity: JSON.stringify([
|
|
456
|
+
provider,
|
|
457
|
+
model,
|
|
458
|
+
text$1(single.baseUrl),
|
|
459
|
+
text$1(single.apiKey)
|
|
460
|
+
])
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
const source = target.source;
|
|
464
|
+
if (kind !== "single" && source !== void 0) {
|
|
465
|
+
for (const step of out) if (step.target.source === void 0) step.target = {
|
|
466
|
+
...step.target,
|
|
467
|
+
source
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
return out;
|
|
471
|
+
}
|
|
472
|
+
//#endregion
|
|
473
|
+
//#region src/classify.ts
|
|
474
|
+
var MAX_CAUSE_DEPTH = 8;
|
|
475
|
+
var QUOTA_CODES = /* @__PURE__ */ new Set([
|
|
476
|
+
"insufficient_quota",
|
|
477
|
+
"billing_hard_limit_reached",
|
|
478
|
+
"billing_not_active",
|
|
479
|
+
"quota_exceeded",
|
|
480
|
+
"subscription_sharing_usage_limit_exceeded"
|
|
481
|
+
]);
|
|
482
|
+
var QUOTA_NAMES = /* @__PURE__ */ new Set(["ChatGPTPlanUsageLimitError"]);
|
|
483
|
+
var CREDENTIAL_CODES = /* @__PURE__ */ new Set([
|
|
484
|
+
"AUTH_ERROR",
|
|
485
|
+
"INFERENCE_TARGET_UNBOUND",
|
|
486
|
+
"invalid_api_key",
|
|
487
|
+
"authentication_error",
|
|
488
|
+
"permission_error",
|
|
489
|
+
"subscription_sharing_invalid_user"
|
|
490
|
+
]);
|
|
491
|
+
var CREDENTIAL_NAMES = /* @__PURE__ */ new Set([
|
|
492
|
+
"AuthenticationError",
|
|
493
|
+
"PermissionDeniedError",
|
|
494
|
+
"AIProviderNotConfiguredError",
|
|
495
|
+
"ChatGPTPlanSessionRevokedError"
|
|
496
|
+
]);
|
|
497
|
+
var MISSING_MODEL_CODES = /* @__PURE__ */ new Set(["MODEL_NOT_FOUND", "model_not_found"]);
|
|
498
|
+
var MISSING_MODEL_NAMES = /* @__PURE__ */ new Set(["ModelNotFoundError", "NotFoundError"]);
|
|
499
|
+
var REFUSAL_CODES = /* @__PURE__ */ new Set(["CONTENT_FILTERED", "content_filter"]);
|
|
500
|
+
var REFUSAL_NAMES = /* @__PURE__ */ new Set(["ContentFilterError"]);
|
|
501
|
+
var RATE_LIMIT_CODES = /* @__PURE__ */ new Set(["RATE_LIMIT", "rate_limit_exceeded"]);
|
|
502
|
+
var RATE_LIMIT_NAMES = /* @__PURE__ */ new Set(["RateLimitError"]);
|
|
503
|
+
var TRANSIENT_CODES = /* @__PURE__ */ new Set([
|
|
504
|
+
"AI_TIMEOUT",
|
|
505
|
+
"AI_INTERRUPTED",
|
|
506
|
+
"STREAM_INTERRUPTED",
|
|
507
|
+
"API_ERROR",
|
|
508
|
+
"UNKNOWN_ERROR",
|
|
509
|
+
"overloaded_error",
|
|
510
|
+
"ECONNRESET",
|
|
511
|
+
"ECONNREFUSED",
|
|
512
|
+
"ECONNABORTED",
|
|
513
|
+
"ETIMEDOUT",
|
|
514
|
+
"EPIPE",
|
|
515
|
+
"EAI_AGAIN",
|
|
516
|
+
"ENOTFOUND",
|
|
517
|
+
"ENETUNREACH",
|
|
518
|
+
"EHOSTUNREACH",
|
|
519
|
+
"UND_ERR_SOCKET",
|
|
520
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
521
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
522
|
+
"UND_ERR_BODY_TIMEOUT"
|
|
523
|
+
]);
|
|
524
|
+
var TRANSIENT_NAMES = /* @__PURE__ */ new Set([
|
|
525
|
+
"TimeoutError",
|
|
526
|
+
"APIConnectionError",
|
|
527
|
+
"APIConnectionTimeoutError",
|
|
528
|
+
"InternalServerError"
|
|
529
|
+
]);
|
|
530
|
+
var TRANSIENT_STATUSES = /* @__PURE__ */ new Set([
|
|
531
|
+
408,
|
|
532
|
+
409,
|
|
533
|
+
425,
|
|
534
|
+
500,
|
|
535
|
+
502,
|
|
536
|
+
503,
|
|
537
|
+
504,
|
|
538
|
+
529
|
|
539
|
+
]);
|
|
540
|
+
var REQUEST_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
541
|
+
"invalid_request_error",
|
|
542
|
+
"CONTEXT_LENGTH_EXCEEDED",
|
|
543
|
+
"context_length_exceeded",
|
|
544
|
+
"INVALID_INPUT"
|
|
545
|
+
]);
|
|
546
|
+
var ABORT_CODES = /* @__PURE__ */ new Set(["AI_ABORTED", "ABORT_ERR"]);
|
|
547
|
+
var ABORT_NAMES = /* @__PURE__ */ new Set(["AbortError"]);
|
|
548
|
+
function asObject(value) {
|
|
549
|
+
return value !== null && typeof value === "object" ? value : void 0;
|
|
550
|
+
}
|
|
551
|
+
function collectFacts(error) {
|
|
552
|
+
const facts = {
|
|
553
|
+
names: /* @__PURE__ */ new Set(),
|
|
554
|
+
codes: /* @__PURE__ */ new Set(),
|
|
555
|
+
statuses: /* @__PURE__ */ new Set(),
|
|
556
|
+
rateLimitNotSoon: false,
|
|
557
|
+
retryable: false
|
|
558
|
+
};
|
|
559
|
+
const seen = /* @__PURE__ */ new Set();
|
|
560
|
+
const queue = [{
|
|
561
|
+
value: error,
|
|
562
|
+
depth: 0
|
|
563
|
+
}];
|
|
564
|
+
while (queue.length > 0) {
|
|
565
|
+
const { value, depth } = queue.shift();
|
|
566
|
+
const record = asObject(value);
|
|
567
|
+
if (!record || seen.has(record) || depth > MAX_CAUSE_DEPTH) continue;
|
|
568
|
+
seen.add(record);
|
|
569
|
+
const name = typeof record.name === "string" ? record.name : value.constructor?.name;
|
|
570
|
+
if (typeof name === "string" && name !== "Error" && name !== "Object") facts.names.add(name);
|
|
571
|
+
for (const key of ["code", "type"]) {
|
|
572
|
+
const code = record[key];
|
|
573
|
+
if (typeof code === "string" && code !== "") facts.codes.add(code);
|
|
574
|
+
}
|
|
575
|
+
for (const key of ["status", "statusCode"]) {
|
|
576
|
+
const status = record[key];
|
|
577
|
+
if (typeof status === "number" && Number.isInteger(status)) facts.statuses.add(status);
|
|
578
|
+
}
|
|
579
|
+
const response = asObject(record.response);
|
|
580
|
+
if (typeof response?.status === "number") facts.statuses.add(response.status);
|
|
581
|
+
if (record.retryable === true) facts.retryable = true;
|
|
582
|
+
if ((RATE_LIMIT_NAMES.has(String(name)) || record.code === "RATE_LIMIT") && record.retryable === false) facts.rateLimitNotSoon = true;
|
|
583
|
+
for (const next of [
|
|
584
|
+
record.cause,
|
|
585
|
+
record.error,
|
|
586
|
+
asObject(record.context)?.originalError
|
|
587
|
+
]) if (next !== void 0) queue.push({
|
|
588
|
+
value: next,
|
|
589
|
+
depth: depth + 1
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
return facts;
|
|
593
|
+
}
|
|
594
|
+
function hasAny(set, members) {
|
|
595
|
+
for (const value of set) if (members.has(value)) return true;
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
function isInferenceAbort(error) {
|
|
599
|
+
const facts = collectFacts(error);
|
|
600
|
+
return hasAny(facts.codes, ABORT_CODES) || hasAny(facts.names, ABORT_NAMES);
|
|
601
|
+
}
|
|
602
|
+
function classifyInferenceFailure(error) {
|
|
603
|
+
const unavailable = error;
|
|
604
|
+
if (unavailable && typeof unavailable === "object" && unavailable.code === "AI_UNAVAILABLE" && unavailable.reason === "capability") return "missing-capability";
|
|
605
|
+
const facts = collectFacts(error);
|
|
606
|
+
const { names, codes, statuses } = facts;
|
|
607
|
+
if (hasAny(codes, ABORT_CODES) || hasAny(names, ABORT_NAMES)) return;
|
|
608
|
+
if (hasAny(codes, QUOTA_CODES) || hasAny(names, QUOTA_NAMES) || statuses.has(402)) return "quota";
|
|
609
|
+
if (hasAny(codes, CREDENTIAL_CODES) || hasAny(names, CREDENTIAL_NAMES) || statuses.has(401) || statuses.has(403)) return "credential";
|
|
610
|
+
if (hasAny(codes, MISSING_MODEL_CODES) || hasAny(names, MISSING_MODEL_NAMES) || statuses.has(404)) return "missing-model";
|
|
611
|
+
if (hasAny(codes, REFUSAL_CODES) || hasAny(names, REFUSAL_NAMES)) return "refusal";
|
|
612
|
+
if (hasAny(codes, RATE_LIMIT_CODES) || hasAny(names, RATE_LIMIT_NAMES) || statuses.has(429)) return facts.rateLimitNotSoon ? "quota" : "transient";
|
|
613
|
+
for (const status of statuses) if (TRANSIENT_STATUSES.has(status)) return "transient";
|
|
614
|
+
for (const status of statuses) if (status >= 400 && status < 500) return void 0;
|
|
615
|
+
if (hasAny(codes, REQUEST_ERROR_CODES)) return void 0;
|
|
616
|
+
if (hasAny(codes, TRANSIENT_CODES) || hasAny(names, TRANSIENT_NAMES) || facts.retryable) return "transient";
|
|
617
|
+
}
|
|
618
|
+
//#endregion
|
|
619
|
+
//#region src/executor.ts
|
|
620
|
+
var RefusalSignal = class extends Error {
|
|
621
|
+
code = "CONTENT_FILTERED";
|
|
622
|
+
};
|
|
623
|
+
var MissingCapabilitySignal = class extends Error {
|
|
624
|
+
constructor(label, missing, source) {
|
|
625
|
+
super(`${label} lacks ${missing.join(" and ")} (capabilities: ${source})`);
|
|
626
|
+
this.missing = missing;
|
|
627
|
+
this.source = source;
|
|
628
|
+
this.name = "MissingCapabilitySignal";
|
|
629
|
+
}
|
|
630
|
+
missing;
|
|
631
|
+
source;
|
|
632
|
+
code = "MISSING_CAPABILITY";
|
|
633
|
+
};
|
|
634
|
+
function wantsJson(options) {
|
|
635
|
+
const type = options.responseFormat?.type;
|
|
636
|
+
return type === "json_object" || type === "json_schema";
|
|
637
|
+
}
|
|
638
|
+
function wantsTools(options) {
|
|
639
|
+
return Array.isArray(options.tools) && options.tools.length > 0;
|
|
640
|
+
}
|
|
641
|
+
var REFUSAL_FINISH = /* @__PURE__ */ new Set(["refusal", "content_filter"]);
|
|
642
|
+
function lowerTier(tier) {
|
|
643
|
+
const index = INFERENCE_TIERS.indexOf(tier);
|
|
644
|
+
return index > 0 ? INFERENCE_TIERS[index - 1] : void 0;
|
|
645
|
+
}
|
|
646
|
+
function defaultSleep(ms) {
|
|
647
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
648
|
+
}
|
|
649
|
+
function method(client, name) {
|
|
650
|
+
const fn = client[name];
|
|
651
|
+
if (typeof fn !== "function") throw new TypeError(`Inference client does not implement ${name}().`);
|
|
652
|
+
return fn.bind(client);
|
|
653
|
+
}
|
|
654
|
+
var ChainCall = class {
|
|
655
|
+
constructor(context, options, operation, callerOptions) {
|
|
656
|
+
this.context = context;
|
|
657
|
+
this.options = options;
|
|
658
|
+
this.operation = operation;
|
|
659
|
+
this.callerOptions = callerOptions;
|
|
660
|
+
this.started = options.now();
|
|
661
|
+
}
|
|
662
|
+
context;
|
|
663
|
+
options;
|
|
664
|
+
operation;
|
|
665
|
+
callerOptions;
|
|
666
|
+
started;
|
|
667
|
+
substitutions = [];
|
|
668
|
+
lastStep;
|
|
669
|
+
get tierRequested() {
|
|
670
|
+
return this.callerOptions.usageTags?.tierRequested ?? this.context.request.tier;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Check a candidate's capabilities against the request before calling it
|
|
674
|
+
* (#4758). Without a detector (older core) or a requirement, it fits.
|
|
675
|
+
*/
|
|
676
|
+
async capabilityFit(step) {
|
|
677
|
+
const effective = {
|
|
678
|
+
...step.params ?? {},
|
|
679
|
+
...this.callerOptions
|
|
680
|
+
};
|
|
681
|
+
const json = wantsJson(effective);
|
|
682
|
+
const tools = wantsTools(effective);
|
|
683
|
+
if (!json && !tools || !this.context.capabilitiesFor) return { ok: true };
|
|
684
|
+
let capabilities;
|
|
685
|
+
try {
|
|
686
|
+
capabilities = await this.context.capabilitiesFor(step.target);
|
|
687
|
+
} catch {
|
|
688
|
+
return { ok: true };
|
|
689
|
+
}
|
|
690
|
+
const fallback = this.callerOptions.capabilityFallback ?? {};
|
|
691
|
+
const adjust = { fallbacks: [] };
|
|
692
|
+
const missing = [];
|
|
693
|
+
if (json && !capabilities.json) if (fallback.json === "text") {
|
|
694
|
+
adjust.dropJson = true;
|
|
695
|
+
adjust.fallbacks.push("json→text");
|
|
696
|
+
} else missing.push("json");
|
|
697
|
+
if (tools && !capabilities.tools) if (fallback.tools === "drop") {
|
|
698
|
+
adjust.dropTools = true;
|
|
699
|
+
adjust.fallbacks.push("tools→none");
|
|
700
|
+
} else missing.push("tools");
|
|
701
|
+
if (missing.length > 0) return {
|
|
702
|
+
ok: false,
|
|
703
|
+
error: new MissingCapabilitySignal(step.label, missing, capabilities.source)
|
|
704
|
+
};
|
|
705
|
+
return adjust.fallbacks.length > 0 ? {
|
|
706
|
+
ok: true,
|
|
707
|
+
adjust
|
|
708
|
+
} : { ok: true };
|
|
709
|
+
}
|
|
710
|
+
callOptions(step, emit, finish, adjust) {
|
|
711
|
+
const { usageTags: callerTags, onProgress, onFinishReason, onRefusal, capabilityFallback: _capabilityFallback, ...rest } = this.callerOptions;
|
|
712
|
+
const model = typeof step.target.model === "string" ? step.target.model : void 0;
|
|
713
|
+
const options = {
|
|
714
|
+
...step.params ?? {},
|
|
715
|
+
...rest,
|
|
716
|
+
...model ? { model } : {},
|
|
717
|
+
usageTags: {
|
|
718
|
+
...callerTags ?? {},
|
|
719
|
+
...typeof step.target.source === "string" && step.target.source ? { source: step.target.source } : {},
|
|
720
|
+
tierRequested: this.tierRequested,
|
|
721
|
+
tierResolved: step.tier,
|
|
722
|
+
...this.substitutions.length > 0 ? { substitutions: JSON.stringify(this.substitutions) } : {},
|
|
723
|
+
...adjust && adjust.fallbacks.length > 0 ? { capabilityFallback: adjust.fallbacks.join(",") } : {}
|
|
724
|
+
},
|
|
725
|
+
...onProgress ? { onProgress: (chunk) => {
|
|
726
|
+
emit(chunk);
|
|
727
|
+
onProgress(chunk);
|
|
728
|
+
} } : {},
|
|
729
|
+
onFinishReason: (reason) => {
|
|
730
|
+
finish.reason = reason;
|
|
731
|
+
if (onFinishReason) finish.replay.push(() => onFinishReason(reason));
|
|
732
|
+
},
|
|
733
|
+
...onRefusal ? { onRefusal: (refusal) => {
|
|
734
|
+
finish.replay.push(() => onRefusal(refusal));
|
|
735
|
+
} } : {}
|
|
736
|
+
};
|
|
737
|
+
if (adjust?.dropJson) delete options.responseFormat;
|
|
738
|
+
if (adjust?.dropTools) delete options.tools;
|
|
739
|
+
return options;
|
|
740
|
+
}
|
|
741
|
+
async attemptOnce(step, invoke, isRefusal, adjust) {
|
|
742
|
+
let emitted = "";
|
|
743
|
+
const emit = (chunk) => {
|
|
744
|
+
emitted += chunk;
|
|
745
|
+
};
|
|
746
|
+
const finish = { replay: [] };
|
|
747
|
+
try {
|
|
748
|
+
const value = await invoke(await this.context.clientFor(step.target), this.callOptions(step, emit, finish, adjust), emit);
|
|
749
|
+
if ((finish.reason !== void 0 && REFUSAL_FINISH.has(finish.reason) || (isRefusal?.(value) ?? false)) && emitted === "") return {
|
|
750
|
+
ok: false,
|
|
751
|
+
cls: "refusal",
|
|
752
|
+
error: new RefusalSignal(`${step.label} declined the request`),
|
|
753
|
+
emitted
|
|
754
|
+
};
|
|
755
|
+
for (const replay of finish.replay) replay();
|
|
756
|
+
return {
|
|
757
|
+
ok: true,
|
|
758
|
+
value
|
|
759
|
+
};
|
|
760
|
+
} catch (error) {
|
|
761
|
+
return {
|
|
762
|
+
ok: false,
|
|
763
|
+
cls: classifyInferenceFailure(error),
|
|
764
|
+
error,
|
|
765
|
+
emitted
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
async attempt(step, invoke, isRefusal, adjust) {
|
|
770
|
+
const first = await this.attemptOnce(step, invoke, isRefusal, adjust);
|
|
771
|
+
if (first.ok || first.cls !== "transient" || first.emitted !== "") return first;
|
|
772
|
+
await this.options.sleep(this.options.retryDelayMs());
|
|
773
|
+
return this.attemptOnce(step, invoke, isRefusal, adjust);
|
|
774
|
+
}
|
|
775
|
+
async recordFailure(outcome, reason) {
|
|
776
|
+
const step = this.lastStep;
|
|
777
|
+
const { tierResolved: _unanswered, ...callerTags } = this.callerOptions.usageTags ?? {};
|
|
778
|
+
try {
|
|
779
|
+
await this.context.recordUsage({
|
|
780
|
+
...typeof step?.target.provider === "string" ? { provider: step.target.provider } : {},
|
|
781
|
+
...typeof step?.target.model === "string" ? { model: step.target.model } : {},
|
|
782
|
+
operation: this.operation,
|
|
783
|
+
duration: Math.max(0, this.options.now() - this.started),
|
|
784
|
+
tags: {
|
|
785
|
+
...callerTags,
|
|
786
|
+
tierRequested: this.tierRequested,
|
|
787
|
+
outcome,
|
|
788
|
+
reason,
|
|
789
|
+
substitutions: JSON.stringify(this.substitutions)
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
} catch {}
|
|
793
|
+
}
|
|
794
|
+
async run(invoke, isRefusal, partialValue) {
|
|
795
|
+
const { context } = this;
|
|
796
|
+
const attempted = /* @__PURE__ */ new Set();
|
|
797
|
+
let pending;
|
|
798
|
+
let tier = context.resolution.tierResolved;
|
|
799
|
+
let steps = expandResolved(context.resolution.target, tier, context.config);
|
|
800
|
+
for (;;) {
|
|
801
|
+
for (const step of steps) {
|
|
802
|
+
if (attempted.has(step.identity)) continue;
|
|
803
|
+
attempted.add(step.identity);
|
|
804
|
+
if (pending) this.substitutions.push({
|
|
805
|
+
from: pending.label,
|
|
806
|
+
to: step.label,
|
|
807
|
+
reason: pending.reason
|
|
808
|
+
});
|
|
809
|
+
pending = void 0;
|
|
810
|
+
this.lastStep = step;
|
|
811
|
+
if (step.unavailable) {
|
|
812
|
+
pending = {
|
|
813
|
+
label: step.label,
|
|
814
|
+
reason: step.unavailable
|
|
815
|
+
};
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
const fit = await this.capabilityFit(step);
|
|
819
|
+
if (!fit.ok) {
|
|
820
|
+
pending = {
|
|
821
|
+
label: step.label,
|
|
822
|
+
reason: "missing-capability",
|
|
823
|
+
error: fit.error
|
|
824
|
+
};
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
const outcome = await this.attempt(step, invoke, isRefusal, fit.adjust);
|
|
828
|
+
if (outcome.ok) return outcome.value;
|
|
829
|
+
if (outcome.emitted !== "") {
|
|
830
|
+
this.substitutions.push({
|
|
831
|
+
from: step.label,
|
|
832
|
+
to: null,
|
|
833
|
+
reason: "partial"
|
|
834
|
+
});
|
|
835
|
+
await this.recordFailure("partial", outcome.cls ?? "unclassified");
|
|
836
|
+
if (context.allowPartial && partialValue) return partialValue(outcome.emitted);
|
|
837
|
+
throw new InferenceExhaustedError({
|
|
838
|
+
reason: "partial",
|
|
839
|
+
substitutions: [...this.substitutions],
|
|
840
|
+
tierRequested: this.tierRequested,
|
|
841
|
+
partialText: outcome.emitted,
|
|
842
|
+
cause: outcome.error
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
if (outcome.cls === void 0) {
|
|
846
|
+
if (!isInferenceAbort(outcome.error) && !isInferenceUnavailableError(outcome.error)) {
|
|
847
|
+
this.substitutions.push({
|
|
848
|
+
from: step.label,
|
|
849
|
+
to: null,
|
|
850
|
+
reason: "unclassified"
|
|
851
|
+
});
|
|
852
|
+
await this.recordFailure("failed", "unclassified");
|
|
853
|
+
}
|
|
854
|
+
throw outcome.error;
|
|
855
|
+
}
|
|
856
|
+
pending = {
|
|
857
|
+
label: step.label,
|
|
858
|
+
reason: outcome.cls,
|
|
859
|
+
error: outcome.error
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
if (context.config?.degrade === false) break;
|
|
863
|
+
const next = lowerTier(tier);
|
|
864
|
+
if (!next) break;
|
|
865
|
+
if (context.minimumTier && compareInferenceTiers(next, context.minimumTier) < 0) break;
|
|
866
|
+
const resolution = await context.resolveTier(next);
|
|
867
|
+
if (resolution.unavailable) break;
|
|
868
|
+
tier = resolution.tierResolved;
|
|
869
|
+
steps = expandResolved(resolution.target, tier, context.config);
|
|
870
|
+
}
|
|
871
|
+
if (pending) this.substitutions.push({
|
|
872
|
+
from: pending.label,
|
|
873
|
+
to: null,
|
|
874
|
+
reason: pending.reason
|
|
875
|
+
});
|
|
876
|
+
const reason = pending?.reason ?? "unclassified";
|
|
877
|
+
await this.recordFailure("failed", reason);
|
|
878
|
+
throw new InferenceExhaustedError({
|
|
879
|
+
reason,
|
|
880
|
+
substitutions: [...this.substitutions],
|
|
881
|
+
tierRequested: this.tierRequested,
|
|
882
|
+
cause: pending?.error
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
function expandResolved(target, tier, config) {
|
|
887
|
+
validateInferenceTarget(target, `resolution.${tier}`, config);
|
|
888
|
+
return expandTarget(target, tier, config);
|
|
889
|
+
}
|
|
890
|
+
function createChunkQueue(onClose) {
|
|
891
|
+
const chunks = [];
|
|
892
|
+
let wake;
|
|
893
|
+
let done = false;
|
|
894
|
+
let closed = false;
|
|
895
|
+
let failure;
|
|
896
|
+
return {
|
|
897
|
+
/** True once the consumer stopped reading. */
|
|
898
|
+
get closed() {
|
|
899
|
+
return closed;
|
|
900
|
+
},
|
|
901
|
+
push(chunk) {
|
|
902
|
+
if (closed) return;
|
|
903
|
+
chunks.push(chunk);
|
|
904
|
+
wake?.();
|
|
905
|
+
},
|
|
906
|
+
end(error) {
|
|
907
|
+
done = true;
|
|
908
|
+
failure = error;
|
|
909
|
+
wake?.();
|
|
910
|
+
},
|
|
911
|
+
async *drain() {
|
|
912
|
+
try {
|
|
913
|
+
for (;;) {
|
|
914
|
+
if (chunks.length > 0) {
|
|
915
|
+
yield chunks.shift();
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
if (done) {
|
|
919
|
+
if (failure) throw failure.error;
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
await new Promise((resolve) => {
|
|
923
|
+
wake = resolve;
|
|
924
|
+
});
|
|
925
|
+
wake = void 0;
|
|
926
|
+
}
|
|
927
|
+
} finally {
|
|
928
|
+
if (!done) {
|
|
929
|
+
closed = true;
|
|
930
|
+
chunks.length = 0;
|
|
931
|
+
onClose();
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
function isRefusalResponse(value) {
|
|
938
|
+
const reason = value?.finishReason;
|
|
939
|
+
return typeof reason === "string" && REFUSAL_FINISH.has(reason);
|
|
940
|
+
}
|
|
941
|
+
var lastValidated;
|
|
942
|
+
function validateChainConfig(config) {
|
|
943
|
+
const key = JSON.stringify(config ?? null);
|
|
944
|
+
if (key === lastValidated) return;
|
|
945
|
+
validateInferenceConfig(config);
|
|
946
|
+
lastValidated = key;
|
|
947
|
+
}
|
|
948
|
+
function createChainClient(context, options = {}) {
|
|
949
|
+
validateChainConfig(context.config);
|
|
950
|
+
const resolved = {
|
|
951
|
+
retryDelayMs: options.retryDelayMs ?? (() => 200 + Math.random() * 300),
|
|
952
|
+
sleep: options.sleep ?? defaultSleep,
|
|
953
|
+
now: options.now ?? (() => Date.now())
|
|
954
|
+
};
|
|
955
|
+
const call = (operation, callerOptions) => new ChainCall(context, resolved, operation, callerOptions ?? {});
|
|
956
|
+
return {
|
|
957
|
+
message(text, callerOptions) {
|
|
958
|
+
return call("message", callerOptions).run((client, opts) => client.message(text, opts), void 0, (partial) => partial);
|
|
959
|
+
},
|
|
960
|
+
chat(messages, callerOptions) {
|
|
961
|
+
return call("chat", callerOptions).run((client, opts) => Promise.resolve(method(client, "chat")(messages, opts)), isRefusalResponse);
|
|
962
|
+
},
|
|
963
|
+
complete(prompt, callerOptions) {
|
|
964
|
+
return call("complete", callerOptions).run((client, opts) => Promise.resolve(method(client, "complete")(prompt, opts)), isRefusalResponse);
|
|
965
|
+
},
|
|
966
|
+
stream(messages, callerOptions) {
|
|
967
|
+
const controller = new AbortController();
|
|
968
|
+
const queue = createChunkQueue(() => controller.abort());
|
|
969
|
+
const callerSignal = callerOptions?.signal;
|
|
970
|
+
const signal = callerSignal ? AbortSignal.any([callerSignal, controller.signal]) : controller.signal;
|
|
971
|
+
call("stream", {
|
|
972
|
+
...callerOptions ?? {},
|
|
973
|
+
signal
|
|
974
|
+
}).run(async (client, opts, emit) => {
|
|
975
|
+
const iterable = method(client, "stream")(messages, opts);
|
|
976
|
+
for await (const chunk of iterable) {
|
|
977
|
+
if (queue.closed) break;
|
|
978
|
+
emit(chunk);
|
|
979
|
+
queue.push(chunk);
|
|
980
|
+
}
|
|
981
|
+
}, void 0, () => void 0).then(() => queue.end(), (error) => queue.end({ error }));
|
|
982
|
+
return queue.drain();
|
|
983
|
+
},
|
|
984
|
+
async embed(...args) {
|
|
985
|
+
const steps = expandTarget(context.resolution.target, context.resolution.tierResolved, context.config);
|
|
986
|
+
const step = steps.find((candidate) => !candidate.unavailable);
|
|
987
|
+
if (!step) throw new InferenceExhaustedError({
|
|
988
|
+
reason: steps[0]?.unavailable ?? "unclassified",
|
|
989
|
+
substitutions: [],
|
|
990
|
+
tierRequested: context.request.tier
|
|
991
|
+
});
|
|
992
|
+
return method(await context.clientFor(step.target), "embed")(...args);
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
//#endregion
|
|
997
|
+
//#region src/fusion.ts
|
|
998
|
+
function defineFusion(input) {
|
|
999
|
+
const key = typeof input?.key === "string" ? input.key.trim() : "";
|
|
1000
|
+
if (!key) throw new InferenceConfigError([{
|
|
1001
|
+
path: "fusions",
|
|
1002
|
+
code: "fusion-invalid",
|
|
1003
|
+
message: "defineFusion() needs a non-empty key"
|
|
1004
|
+
}]);
|
|
1005
|
+
const { key: _key, ...definition } = input;
|
|
1006
|
+
const path = `fusions.${key}`;
|
|
1007
|
+
const issues = findFusionDefinitionIssues(definition, path, void 0).filter((issue) => issue.code !== "alias-unknown");
|
|
1008
|
+
issues.push(...findFusionDefinitionPromptIssues(definition, path));
|
|
1009
|
+
if (issues.length > 0) throw new InferenceConfigError(issues);
|
|
1010
|
+
const store = fusionStore();
|
|
1011
|
+
const existing = store.get(key);
|
|
1012
|
+
if (existing) {
|
|
1013
|
+
if (JSON.stringify(existing) !== JSON.stringify(definition)) throw new InferenceConfigError([{
|
|
1014
|
+
path,
|
|
1015
|
+
code: "fusion-invalid",
|
|
1016
|
+
message: `fusion "${key}" is already defined with a different definition`
|
|
1017
|
+
}]);
|
|
1018
|
+
return existing;
|
|
1019
|
+
}
|
|
1020
|
+
const frozen = structuredClone(definition);
|
|
1021
|
+
store.set(key, frozen);
|
|
1022
|
+
return frozen;
|
|
1023
|
+
}
|
|
1024
|
+
function getFusion(key) {
|
|
1025
|
+
return fusionStore().get(key);
|
|
1026
|
+
}
|
|
1027
|
+
function listFusions() {
|
|
1028
|
+
return [...fusionStore().keys()];
|
|
1029
|
+
}
|
|
1030
|
+
function clearFusions() {
|
|
1031
|
+
fusionStore().clear();
|
|
1032
|
+
}
|
|
1033
|
+
var RESOLVER_KEY = /* @__PURE__ */ Symbol.for("smrt.inference.fusionPromptResolver");
|
|
1034
|
+
function setFusionPromptResolver(resolver) {
|
|
1035
|
+
const holder = globalThis;
|
|
1036
|
+
if (resolver) holder[RESOLVER_KEY] = resolver;
|
|
1037
|
+
else delete holder[RESOLVER_KEY];
|
|
1038
|
+
}
|
|
1039
|
+
var PROMPTS_SPECIFIER = "@happyvertical/smrt-prompts";
|
|
1040
|
+
async function loadPrompts() {
|
|
1041
|
+
try {
|
|
1042
|
+
return await import(
|
|
1043
|
+
/* @vite-ignore */
|
|
1044
|
+
PROMPTS_SPECIFIER
|
|
1045
|
+
);
|
|
1046
|
+
} catch {
|
|
1047
|
+
return await importOptionalDependency(PROMPTS_SPECIFIER, "Fusion seats resolve prompts through @happyvertical/smrt-prompts; install it, register it with registerOptionalDependency(), or call setFusionPromptResolver().");
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
var defaultPromptResolver = async (key, options) => {
|
|
1051
|
+
const resolved = await (await loadPrompts()).resolvePrompt(key, {
|
|
1052
|
+
...options.tenantId !== void 0 ? { tenantId: options.tenantId } : {},
|
|
1053
|
+
...options.db ? { db: options.db } : {}
|
|
1054
|
+
});
|
|
1055
|
+
const params = {};
|
|
1056
|
+
if (typeof resolved.ai.temperature === "number") params.temperature = resolved.ai.temperature;
|
|
1057
|
+
if (typeof resolved.ai.maxTokens === "number") params.maxTokens = resolved.ai.maxTokens;
|
|
1058
|
+
return {
|
|
1059
|
+
instructions: resolved.text,
|
|
1060
|
+
...resolved.ai.usageTags ? { usageTags: resolved.ai.usageTags } : {},
|
|
1061
|
+
params
|
|
1062
|
+
};
|
|
1063
|
+
};
|
|
1064
|
+
function promptResolver() {
|
|
1065
|
+
return globalThis[RESOLVER_KEY] ?? defaultPromptResolver;
|
|
1066
|
+
}
|
|
1067
|
+
var ENTRY_TAGS = /* @__PURE__ */ new Set([
|
|
1068
|
+
"promptKey",
|
|
1069
|
+
"promptLayer",
|
|
1070
|
+
"templateHash",
|
|
1071
|
+
"tierRequested",
|
|
1072
|
+
"tierResolved",
|
|
1073
|
+
"substitutions",
|
|
1074
|
+
"runId",
|
|
1075
|
+
"fusionKey",
|
|
1076
|
+
"seat"
|
|
1077
|
+
]);
|
|
1078
|
+
var TRANSPORT_OPTIONS = ["signal", "timeout"];
|
|
1079
|
+
var MAX_MESSAGE = 300;
|
|
1080
|
+
function seatName(seat) {
|
|
1081
|
+
return (seat.name ?? seat.prompt).trim();
|
|
1082
|
+
}
|
|
1083
|
+
function targetLabel(target) {
|
|
1084
|
+
if (typeof target === "string") return `tier:${target}`;
|
|
1085
|
+
const kind = inferenceTargetKind(target);
|
|
1086
|
+
if (kind === "alias") return `alias:${String(target.alias)}`;
|
|
1087
|
+
if (kind === "candidates") return "candidates";
|
|
1088
|
+
return candidateLabel({
|
|
1089
|
+
...typeof target.provider === "string" ? { provider: target.provider } : {},
|
|
1090
|
+
...typeof target.model === "string" ? { model: target.model } : {}
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
function shortMessage(error) {
|
|
1094
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1095
|
+
return message.length > MAX_MESSAGE ? `${message.slice(0, MAX_MESSAGE)}\u2026` : message;
|
|
1096
|
+
}
|
|
1097
|
+
function errorCode(error) {
|
|
1098
|
+
const code = error?.code;
|
|
1099
|
+
return typeof code === "string" ? code : void 0;
|
|
1100
|
+
}
|
|
1101
|
+
function parseSubstitutions(value) {
|
|
1102
|
+
if (typeof value !== "string" || value === "") return void 0;
|
|
1103
|
+
try {
|
|
1104
|
+
const parsed = JSON.parse(value);
|
|
1105
|
+
return Array.isArray(parsed) && parsed.length > 0 ? parsed : void 0;
|
|
1106
|
+
} catch {
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
function newRunId() {
|
|
1111
|
+
const crypto = globalThis.crypto;
|
|
1112
|
+
if (crypto?.randomUUID) return crypto.randomUUID();
|
|
1113
|
+
return `run-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1114
|
+
}
|
|
1115
|
+
function seatOutputText(seat) {
|
|
1116
|
+
if (seat.raw !== void 0) return seat.raw;
|
|
1117
|
+
return seat.text ?? "";
|
|
1118
|
+
}
|
|
1119
|
+
function seatHeading(seat) {
|
|
1120
|
+
return `seat "${seat.name}" (${seat.model ?? seat.target})`;
|
|
1121
|
+
}
|
|
1122
|
+
function describeSeat(seat, role) {
|
|
1123
|
+
if (seat.status === "failed") {
|
|
1124
|
+
const failure = seat.failure;
|
|
1125
|
+
return `--- ${role} ${seatHeading(seat)} FAILED (${failure?.reason ?? "unclassified"}): ${failure?.message ?? "no answer"} ---
|
|
1126
|
+
This seat produced no output; answer from the other material.`;
|
|
1127
|
+
}
|
|
1128
|
+
const note = seat.raw !== void 0 ? " (asked for JSON; the reply did not parse, raw text follows)" : "";
|
|
1129
|
+
return `--- ${role} ${seatHeading(seat)}${note} ---
|
|
1130
|
+
${seatOutputText(seat)}
|
|
1131
|
+
--- end of ${seat.name} ---`;
|
|
1132
|
+
}
|
|
1133
|
+
var FusionRun = class {
|
|
1134
|
+
constructor(context, key, definition, callerOptions, options) {
|
|
1135
|
+
this.context = context;
|
|
1136
|
+
this.key = key;
|
|
1137
|
+
this.definition = definition;
|
|
1138
|
+
this.options = options;
|
|
1139
|
+
this.started = options.now();
|
|
1140
|
+
const { usageTags, model: _model, ...rest } = callerOptions;
|
|
1141
|
+
this.callerTags = Object.fromEntries(Object.entries(usageTags ?? {}).filter(([tag]) => !ENTRY_TAGS.has(tag)));
|
|
1142
|
+
this.finalOptions = rest;
|
|
1143
|
+
this.transport = {};
|
|
1144
|
+
for (const name of TRANSPORT_OPTIONS) if (rest[name] !== void 0) this.transport[name] = rest[name];
|
|
1145
|
+
this.signal = rest.signal;
|
|
1146
|
+
this.entryPromptKey = context.request.promptKey ?? (typeof usageTags?.promptKey === "string" && usageTags.promptKey !== "" ? usageTags.promptKey : void 0);
|
|
1147
|
+
this.seats = definition.seats.map((seat) => ({
|
|
1148
|
+
name: seatName(seat),
|
|
1149
|
+
prompt: seat.prompt,
|
|
1150
|
+
status: "not-run",
|
|
1151
|
+
target: targetLabel(seat.target)
|
|
1152
|
+
}));
|
|
1153
|
+
}
|
|
1154
|
+
context;
|
|
1155
|
+
key;
|
|
1156
|
+
definition;
|
|
1157
|
+
options;
|
|
1158
|
+
runId = newRunId();
|
|
1159
|
+
seats;
|
|
1160
|
+
usage = /* @__PURE__ */ new Map();
|
|
1161
|
+
gate;
|
|
1162
|
+
started;
|
|
1163
|
+
callerTags;
|
|
1164
|
+
finalOptions;
|
|
1165
|
+
transport;
|
|
1166
|
+
signal;
|
|
1167
|
+
entryPromptKey;
|
|
1168
|
+
/** Resolve a seat's target into the context its chain runs in. */
|
|
1169
|
+
async seatContext(seat, index, track) {
|
|
1170
|
+
const { context } = this;
|
|
1171
|
+
let resolution;
|
|
1172
|
+
let config = context.config;
|
|
1173
|
+
let tier = context.resolution.tierResolved;
|
|
1174
|
+
const resolveTier = (next) => context.resolveTier(next, { promptKey: seat.prompt });
|
|
1175
|
+
if (typeof seat.target === "string") {
|
|
1176
|
+
tier = seat.target === "auto" ? resolveAutoTier(context.config) : seat.target;
|
|
1177
|
+
resolution = await resolveTier(tier);
|
|
1178
|
+
if (resolution.unavailable) throw new InferenceUnavailableError({
|
|
1179
|
+
reason: resolution.unavailable,
|
|
1180
|
+
...resolution.mode ? { mode: resolution.mode } : {},
|
|
1181
|
+
tier: resolution.tierResolved,
|
|
1182
|
+
owner: `fusion "${this.key}" seat "${seatName(seat)}"`
|
|
1183
|
+
});
|
|
1184
|
+
} else {
|
|
1185
|
+
resolution = {
|
|
1186
|
+
tierResolved: tier,
|
|
1187
|
+
target: { ...seat.target },
|
|
1188
|
+
via: "config"
|
|
1189
|
+
};
|
|
1190
|
+
config = {
|
|
1191
|
+
...context.config ?? {},
|
|
1192
|
+
degrade: false
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
if (inferenceTargetKind(resolution.target) === "fusion") throw new InferenceConfigError([{
|
|
1196
|
+
path: `fusions.${this.key}.seats[${index}].target`,
|
|
1197
|
+
code: "fusion-nested",
|
|
1198
|
+
message: `fusion "${this.key}" seat "${seatName(seat)}" resolved tier "${resolution.tierResolved}" to another fusion; a seat never runs a fusion`
|
|
1199
|
+
}]);
|
|
1200
|
+
return {
|
|
1201
|
+
...context,
|
|
1202
|
+
request: {
|
|
1203
|
+
...context.request,
|
|
1204
|
+
tier,
|
|
1205
|
+
promptKey: seat.prompt
|
|
1206
|
+
},
|
|
1207
|
+
resolution,
|
|
1208
|
+
config,
|
|
1209
|
+
resolveTier,
|
|
1210
|
+
allowPartial: false,
|
|
1211
|
+
clientFor: async (target) => {
|
|
1212
|
+
const client = await context.clientFor(target);
|
|
1213
|
+
return {
|
|
1214
|
+
...client,
|
|
1215
|
+
message: async (text, callOptions) => {
|
|
1216
|
+
const answer = await client.message(text, callOptions);
|
|
1217
|
+
track(target, callOptions ?? {});
|
|
1218
|
+
return answer;
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Run one seat. A model failure kills the seat (recorded, never thrown);
|
|
1226
|
+
* a config error, a caller abort, or a final seat that already streamed
|
|
1227
|
+
* text rethrows.
|
|
1228
|
+
*/
|
|
1229
|
+
async runSeat(index, input, final) {
|
|
1230
|
+
const seat = this.definition.seats[index];
|
|
1231
|
+
const result = this.seats[index];
|
|
1232
|
+
const started = this.options.now();
|
|
1233
|
+
let answered = {
|
|
1234
|
+
target: {},
|
|
1235
|
+
tags: {}
|
|
1236
|
+
};
|
|
1237
|
+
let options = {};
|
|
1238
|
+
result.status = "ok";
|
|
1239
|
+
try {
|
|
1240
|
+
const prompt = await promptResolver()(seat.prompt, {
|
|
1241
|
+
...this.context.request.tenantId !== void 0 ? { tenantId: this.context.request.tenantId } : {},
|
|
1242
|
+
...this.context.db ? { db: this.context.db } : {}
|
|
1243
|
+
});
|
|
1244
|
+
const chain = createChainClient(await this.seatContext(seat, index, (target2, opts) => {
|
|
1245
|
+
answered = {
|
|
1246
|
+
target: target2,
|
|
1247
|
+
tags: opts.usageTags ?? {}
|
|
1248
|
+
};
|
|
1249
|
+
}), this.options);
|
|
1250
|
+
const passed = final ? this.finalOptions : this.transport;
|
|
1251
|
+
options = {
|
|
1252
|
+
...prompt.params ?? {},
|
|
1253
|
+
...passed,
|
|
1254
|
+
...seat.temperature !== void 0 ? { temperature: seat.temperature } : {},
|
|
1255
|
+
...seat.output === "json" ? {
|
|
1256
|
+
responseFormat: { type: "json_object" },
|
|
1257
|
+
capabilityFallback: {
|
|
1258
|
+
...passed.capabilityFallback ?? {},
|
|
1259
|
+
json: "text"
|
|
1260
|
+
}
|
|
1261
|
+
} : {},
|
|
1262
|
+
history: [{
|
|
1263
|
+
role: "system",
|
|
1264
|
+
content: prompt.instructions
|
|
1265
|
+
}],
|
|
1266
|
+
usageTags: {
|
|
1267
|
+
...this.callerTags,
|
|
1268
|
+
...prompt.usageTags ?? {},
|
|
1269
|
+
promptKey: seat.prompt,
|
|
1270
|
+
runId: this.runId,
|
|
1271
|
+
fusionKey: this.key,
|
|
1272
|
+
seat: result.name
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
const text = await chain.message(input, options);
|
|
1276
|
+
const target = answered.target;
|
|
1277
|
+
result.model = candidateLabel({
|
|
1278
|
+
...typeof target.provider === "string" ? { provider: target.provider } : {},
|
|
1279
|
+
...typeof target.model === "string" ? { model: target.model } : typeof options.model === "string" ? { model: options.model } : {}
|
|
1280
|
+
});
|
|
1281
|
+
const substitutions = parseSubstitutions(answered.tags.substitutions);
|
|
1282
|
+
if (substitutions) result.substitutions = substitutions;
|
|
1283
|
+
if (seat.output === "json") try {
|
|
1284
|
+
result.json = JSON.parse(text);
|
|
1285
|
+
result.text = text;
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
result.raw = text;
|
|
1288
|
+
result.parseError = shortMessage(error);
|
|
1289
|
+
}
|
|
1290
|
+
else result.text = text;
|
|
1291
|
+
} catch (error) {
|
|
1292
|
+
if (error instanceof InferenceConfigError || isInferenceAbort(error) || final && error instanceof InferenceExhaustedError && error.reason === "partial") {
|
|
1293
|
+
result.status = "failed";
|
|
1294
|
+
result.failure = {
|
|
1295
|
+
reason: error instanceof InferenceExhaustedError ? error.reason : error instanceof InferenceConfigError ? "config" : "aborted",
|
|
1296
|
+
message: shortMessage(error)
|
|
1297
|
+
};
|
|
1298
|
+
throw error;
|
|
1299
|
+
}
|
|
1300
|
+
result.status = "failed";
|
|
1301
|
+
const code = errorCode(error);
|
|
1302
|
+
result.failure = {
|
|
1303
|
+
reason: error instanceof InferenceExhaustedError ? error.reason : classifyInferenceFailure(error) ?? "unclassified",
|
|
1304
|
+
message: shortMessage(error),
|
|
1305
|
+
...code ? { code } : {}
|
|
1306
|
+
};
|
|
1307
|
+
if (error instanceof InferenceExhaustedError) result.substitutions = error.substitutions;
|
|
1308
|
+
} finally {
|
|
1309
|
+
result.durationMs = Math.max(0, this.options.now() - started);
|
|
1310
|
+
}
|
|
1311
|
+
return result;
|
|
1312
|
+
}
|
|
1313
|
+
/** The gate: one typed-decision call over the outputs so far. */
|
|
1314
|
+
async runGate(gate, input, outputs) {
|
|
1315
|
+
const live = outputs.filter((seat) => seat.status === "ok");
|
|
1316
|
+
if (live.length === 0) return { status: "not-run" };
|
|
1317
|
+
if (!this.context.decide) return { status: "unavailable" };
|
|
1318
|
+
const choice = gate.sufficientWhen.choice;
|
|
1319
|
+
try {
|
|
1320
|
+
const result = await this.context.decide({
|
|
1321
|
+
state: {
|
|
1322
|
+
input,
|
|
1323
|
+
outputs: live.map((seat) => ({
|
|
1324
|
+
seat: seat.name,
|
|
1325
|
+
model: seat.model ?? seat.target,
|
|
1326
|
+
text: seatOutputText(seat)
|
|
1327
|
+
}))
|
|
1328
|
+
},
|
|
1329
|
+
questions: { gate: {
|
|
1330
|
+
type: "choice",
|
|
1331
|
+
instructions: gate.criteria,
|
|
1332
|
+
criteria: {
|
|
1333
|
+
[choice]: "The outputs so far already satisfy the instructions; the remaining stage is not needed.",
|
|
1334
|
+
[FUSION_GATE_CONTINUE]: "The remaining stage is still needed."
|
|
1335
|
+
}
|
|
1336
|
+
} }
|
|
1337
|
+
}, {
|
|
1338
|
+
usageTags: {
|
|
1339
|
+
...this.callerTags,
|
|
1340
|
+
runId: this.runId,
|
|
1341
|
+
fusionKey: this.key,
|
|
1342
|
+
seat: FUSION_GATE_SEAT
|
|
1343
|
+
},
|
|
1344
|
+
...this.signal ? { signal: this.signal } : {}
|
|
1345
|
+
});
|
|
1346
|
+
if (!result) return { status: "unavailable" };
|
|
1347
|
+
const answer = result.answers.gate;
|
|
1348
|
+
if (!answer || answer.type !== "choice") return {
|
|
1349
|
+
status: "error",
|
|
1350
|
+
message: "decision provider returned no choice for the gate"
|
|
1351
|
+
};
|
|
1352
|
+
return {
|
|
1353
|
+
status: answer.choice === choice && answer.confidence >= gate.sufficientWhen.minConfidence ? "sufficient" : "insufficient",
|
|
1354
|
+
choice: answer.choice,
|
|
1355
|
+
confidence: answer.confidence,
|
|
1356
|
+
model: `${result.provenance.provider}/${result.provenance.model}`
|
|
1357
|
+
};
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
if (isInferenceAbort(error)) throw error;
|
|
1360
|
+
return {
|
|
1361
|
+
status: "error",
|
|
1362
|
+
message: shortMessage(error)
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
skip(index) {
|
|
1367
|
+
const seat = this.seats[index];
|
|
1368
|
+
seat.status = "skipped";
|
|
1369
|
+
}
|
|
1370
|
+
firstLive(...indices) {
|
|
1371
|
+
return indices.map((index) => this.seats[index]).find((seat) => seat.status === "ok");
|
|
1372
|
+
}
|
|
1373
|
+
async interpret(input) {
|
|
1374
|
+
const { definition } = this;
|
|
1375
|
+
if (definition.mode === "single") {
|
|
1376
|
+
const seat = await this.runSeat(0, input, true);
|
|
1377
|
+
return seat.status === "ok" ? seatOutputText(seat) : void 0;
|
|
1378
|
+
}
|
|
1379
|
+
if (definition.mode === "pair") {
|
|
1380
|
+
const settled = await Promise.allSettled([this.runSeat(0, input, false), this.runSeat(1, input, false)]);
|
|
1381
|
+
const rejected = settled.find((entry) => entry.status === "rejected");
|
|
1382
|
+
if (rejected) throw rejected.reason;
|
|
1383
|
+
const [first, second] = settled.map((entry) => entry.value);
|
|
1384
|
+
if (first.status !== "ok" && second.status !== "ok") return void 0;
|
|
1385
|
+
if (definition.gate) {
|
|
1386
|
+
this.gate = await this.runGate(definition.gate, input, [first, second]);
|
|
1387
|
+
if (this.gate.status === "sufficient") {
|
|
1388
|
+
this.skip(2);
|
|
1389
|
+
return seatOutputText(this.firstLive(0, 1));
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
const merge = await this.runSeat(2, [
|
|
1393
|
+
input,
|
|
1394
|
+
"",
|
|
1395
|
+
"Two seats answered the request above in parallel. Merge their outputs into one answer.",
|
|
1396
|
+
describeSeat(first, "Output of"),
|
|
1397
|
+
describeSeat(second, "Output of")
|
|
1398
|
+
].join("\n"), true);
|
|
1399
|
+
if (merge.status === "ok") return seatOutputText(merge);
|
|
1400
|
+
return seatOutputText(this.firstLive(0, 1));
|
|
1401
|
+
}
|
|
1402
|
+
const draft = await this.runSeat(0, input, false);
|
|
1403
|
+
if (definition.gate) {
|
|
1404
|
+
this.gate = await this.runGate(definition.gate, input, [draft]);
|
|
1405
|
+
if (this.gate.status === "sufficient") {
|
|
1406
|
+
this.skip(1);
|
|
1407
|
+
return seatOutputText(draft);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
const answer = await this.runSeat(1, [
|
|
1411
|
+
input,
|
|
1412
|
+
"",
|
|
1413
|
+
"Another seat drafted an answer to the request above. Review the draft and give the final answer.",
|
|
1414
|
+
describeSeat(draft, "Draft from")
|
|
1415
|
+
].join("\n"), true);
|
|
1416
|
+
if (answer.status === "ok") return seatOutputText(answer);
|
|
1417
|
+
if (draft.status === "ok") return seatOutputText(draft);
|
|
1418
|
+
}
|
|
1419
|
+
result(status, text) {
|
|
1420
|
+
for (const seat of this.seats) {
|
|
1421
|
+
const usage = this.usage.get(seat.name);
|
|
1422
|
+
if (usage) seat.usage = usage;
|
|
1423
|
+
}
|
|
1424
|
+
return {
|
|
1425
|
+
runId: this.runId,
|
|
1426
|
+
fusionKey: this.key,
|
|
1427
|
+
mode: this.definition.mode,
|
|
1428
|
+
status,
|
|
1429
|
+
text,
|
|
1430
|
+
seats: this.seats.map((seat) => ({ ...seat })),
|
|
1431
|
+
...this.gate ? { gate: this.gate } : {},
|
|
1432
|
+
durationMs: Math.max(0, this.options.now() - this.started)
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
async record(result) {
|
|
1436
|
+
if (!this.context.recordRun) return;
|
|
1437
|
+
const ran = result.seats.filter((seat) => seat.status === "ok" || seat.status === "failed");
|
|
1438
|
+
try {
|
|
1439
|
+
await this.context.recordRun({
|
|
1440
|
+
id: result.runId,
|
|
1441
|
+
fusionKey: result.fusionKey,
|
|
1442
|
+
mode: result.mode,
|
|
1443
|
+
...this.entryPromptKey ? { promptKey: this.entryPromptKey } : {},
|
|
1444
|
+
seats: ran.length,
|
|
1445
|
+
degradedSeats: ran.filter(isDegraded).length,
|
|
1446
|
+
status: result.status,
|
|
1447
|
+
duration: result.durationMs,
|
|
1448
|
+
details: {
|
|
1449
|
+
seats: result.seats.map((seat) => ({
|
|
1450
|
+
persona: seat.name,
|
|
1451
|
+
prompt: seat.prompt,
|
|
1452
|
+
status: seat.status,
|
|
1453
|
+
target: seat.target,
|
|
1454
|
+
...seat.model ? { model: seat.model } : {},
|
|
1455
|
+
...seat.usage ? { usage: seat.usage } : {},
|
|
1456
|
+
...seat.substitutions ? { substitutions: seat.substitutions } : {},
|
|
1457
|
+
...seat.failure ? { failure: {
|
|
1458
|
+
reason: seat.failure.reason,
|
|
1459
|
+
...seat.failure.code ? { code: seat.failure.code } : {}
|
|
1460
|
+
} } : {},
|
|
1461
|
+
...seat.raw !== void 0 ? { json: "unparsed" } : {},
|
|
1462
|
+
...seat.json !== void 0 ? { json: "parsed" } : {},
|
|
1463
|
+
...seat.durationMs !== void 0 ? { durationMs: seat.durationMs } : {}
|
|
1464
|
+
})),
|
|
1465
|
+
...result.gate ? { gate: {
|
|
1466
|
+
status: result.gate.status,
|
|
1467
|
+
...result.gate.choice ? { choice: result.gate.choice } : {},
|
|
1468
|
+
...result.gate.confidence !== void 0 ? { confidence: result.gate.confidence } : {},
|
|
1469
|
+
...result.gate.model ? { model: result.gate.model } : {}
|
|
1470
|
+
} } : {}
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
} catch {}
|
|
1474
|
+
}
|
|
1475
|
+
async run(input) {
|
|
1476
|
+
const stop = this.context.observeUsage?.(this.runId, (event) => {
|
|
1477
|
+
const seat = event.tags.seat;
|
|
1478
|
+
if (!seat) return;
|
|
1479
|
+
const usage = this.usage.get(seat) ?? {
|
|
1480
|
+
promptTokens: 0,
|
|
1481
|
+
completionTokens: 0,
|
|
1482
|
+
totalTokens: 0
|
|
1483
|
+
};
|
|
1484
|
+
usage.promptTokens += event.promptTokens;
|
|
1485
|
+
usage.completionTokens += event.completionTokens;
|
|
1486
|
+
usage.totalTokens += event.totalTokens;
|
|
1487
|
+
if (event.estimatedCost !== void 0) usage.estimatedCost = (usage.estimatedCost ?? 0) + event.estimatedCost;
|
|
1488
|
+
this.usage.set(seat, usage);
|
|
1489
|
+
});
|
|
1490
|
+
try {
|
|
1491
|
+
let text;
|
|
1492
|
+
try {
|
|
1493
|
+
text = await this.interpret(input);
|
|
1494
|
+
} catch (error) {
|
|
1495
|
+
const failed = this.result("failed", "");
|
|
1496
|
+
await this.record(failed);
|
|
1497
|
+
if (error instanceof InferenceConfigError || isInferenceAbort(error)) throw error;
|
|
1498
|
+
throw new InferenceFusionError(failed, error);
|
|
1499
|
+
}
|
|
1500
|
+
if (text === void 0) {
|
|
1501
|
+
const failed = this.result("failed", "");
|
|
1502
|
+
await this.record(failed);
|
|
1503
|
+
throw new InferenceFusionError(failed);
|
|
1504
|
+
}
|
|
1505
|
+
const degraded = this.seats.some(isDegraded);
|
|
1506
|
+
const done = this.result(degraded ? "degraded" : "ok", text);
|
|
1507
|
+
await this.record(done);
|
|
1508
|
+
return done;
|
|
1509
|
+
} finally {
|
|
1510
|
+
stop?.();
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1514
|
+
function isDegraded(seat) {
|
|
1515
|
+
return seat.status === "failed" || seat.status === "ok" && (seat.substitutions?.length ?? 0) > 0;
|
|
1516
|
+
}
|
|
1517
|
+
function fusionFor(context) {
|
|
1518
|
+
const key = String(context.resolution.target.fusion ?? "").trim();
|
|
1519
|
+
const definition = key ? lookupFusion(key, context.config) : void 0;
|
|
1520
|
+
if (!definition) throw new InferenceConfigError([{
|
|
1521
|
+
path: `resolution.${context.resolution.tierResolved}`,
|
|
1522
|
+
code: "fusion-unknown",
|
|
1523
|
+
message: `tier "${context.resolution.tierResolved}" names unknown fusion "${key}"`
|
|
1524
|
+
}]);
|
|
1525
|
+
const path = Object.hasOwn(context.config?.fusions ?? {}, key) ? `inference.fusions.${key}` : `fusions.${key}`;
|
|
1526
|
+
const issues = [...findFusionDefinitionIssues(definition, path, context.config), ...findFusionDefinitionPromptIssues(definition, path)];
|
|
1527
|
+
if (issues.length > 0) throw new InferenceConfigError(issues);
|
|
1528
|
+
return {
|
|
1529
|
+
key,
|
|
1530
|
+
definition
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
function runFusion(context, input, callerOptions = {}, options = {}) {
|
|
1534
|
+
const { key, definition } = fusionFor(context);
|
|
1535
|
+
return new FusionRun(context, key, definition, callerOptions, {
|
|
1536
|
+
retryDelayMs: options.retryDelayMs ?? (() => 200 + Math.random() * 300),
|
|
1537
|
+
sleep: options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
1538
|
+
now: options.now ?? (() => Date.now())
|
|
1539
|
+
}).run(input);
|
|
1540
|
+
}
|
|
1541
|
+
function unsupported(method) {
|
|
1542
|
+
throw new TypeError(`A fusion target runs through message(); ${method}() is not supported (#4753).`);
|
|
1543
|
+
}
|
|
1544
|
+
function createFusionClient(context, options = {}) {
|
|
1545
|
+
fusionFor(context);
|
|
1546
|
+
return {
|
|
1547
|
+
async message(text, callerOptions) {
|
|
1548
|
+
const { onFusionResult, ...rest } = callerOptions ?? {};
|
|
1549
|
+
const result = await runFusion(context, text, rest, options);
|
|
1550
|
+
if (typeof onFusionResult === "function") onFusionResult(result);
|
|
1551
|
+
return result.text;
|
|
1552
|
+
},
|
|
1553
|
+
chat: () => unsupported("chat"),
|
|
1554
|
+
complete: () => unsupported("complete"),
|
|
1555
|
+
stream: () => unsupported("stream"),
|
|
1556
|
+
embed: () => unsupported("embed")
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
function createChainExecutor(options = {}) {
|
|
1560
|
+
return {
|
|
1561
|
+
kinds: EXECUTOR_KINDS,
|
|
1562
|
+
createClient: (context) => inferenceTargetKind(context.resolution.target) === "fusion" ? createFusionClient(context, options) : createChainClient(context, options)
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
//#endregion
|
|
1566
|
+
//#region src/register.ts
|
|
1567
|
+
var OWN_RESOLVER = /* @__PURE__ */ Symbol.for("smrt.inference.chainResolver");
|
|
1568
|
+
function createInferenceResolver(base) {
|
|
1569
|
+
let validated;
|
|
1570
|
+
const validate = (config) => {
|
|
1571
|
+
if (!hasInferenceChains(config)) return;
|
|
1572
|
+
const key = JSON.stringify(config);
|
|
1573
|
+
if (key === validated) return;
|
|
1574
|
+
validateInferenceConfig(config);
|
|
1575
|
+
validated = key;
|
|
1576
|
+
};
|
|
1577
|
+
const resolver = { async resolve(request, context) {
|
|
1578
|
+
validate(context.config);
|
|
1579
|
+
return base ? base.resolve(request, context) : void 0;
|
|
1580
|
+
} };
|
|
1581
|
+
if (base && typeof base.mode === "function") {
|
|
1582
|
+
const mode = base.mode.bind(base);
|
|
1583
|
+
resolver.mode = (request) => mode(request);
|
|
1584
|
+
}
|
|
1585
|
+
resolver[OWN_RESOLVER] = true;
|
|
1586
|
+
return resolver;
|
|
1587
|
+
}
|
|
1588
|
+
function registerInference(options = {}) {
|
|
1589
|
+
setInferenceExecutor(createChainExecutor(options.executor));
|
|
1590
|
+
const current = getInferenceResolver();
|
|
1591
|
+
if (current[OWN_RESOLVER]) return;
|
|
1592
|
+
setInferenceResolver(createInferenceResolver(current === builtInInferenceResolver ? void 0 : current));
|
|
1593
|
+
}
|
|
1594
|
+
function isInferenceRegistered() {
|
|
1595
|
+
const executor = getInferenceExecutor();
|
|
1596
|
+
return !!executor && executor.kinds.includes("alias");
|
|
1597
|
+
}
|
|
1598
|
+
//#endregion
|
|
1599
|
+
//#region src/provider-config.ts
|
|
1600
|
+
var INFERENCE_PROVIDER_CONFIG_SECRET_TYPE = "inference.provider-config";
|
|
1601
|
+
var INFERENCE_PROVIDER_CONFIG_SOURCES = [
|
|
1602
|
+
"byo",
|
|
1603
|
+
"local",
|
|
1604
|
+
"hosted"
|
|
1605
|
+
];
|
|
1606
|
+
var InferenceProviderConfigError = class extends SmrtError {
|
|
1607
|
+
issues;
|
|
1608
|
+
constructor(issues) {
|
|
1609
|
+
super(`Invalid inference provider config: ${issues.join("; ")}`, "INFERENCE_PROVIDER_CONFIG_INVALID", "configuration", { issues });
|
|
1610
|
+
this.issues = issues;
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
var INFERENCE_PROVIDER_PARAM_KEYS = [
|
|
1614
|
+
"temperature",
|
|
1615
|
+
"maxTokens",
|
|
1616
|
+
"topP",
|
|
1617
|
+
"topK",
|
|
1618
|
+
"frequencyPenalty",
|
|
1619
|
+
"presencePenalty",
|
|
1620
|
+
"seed",
|
|
1621
|
+
"stop"
|
|
1622
|
+
];
|
|
1623
|
+
var PARAM_KEYS = INFERENCE_PROVIDER_PARAM_KEYS;
|
|
1624
|
+
function isParamValue(value) {
|
|
1625
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
1626
|
+
if (typeof value === "string") return true;
|
|
1627
|
+
return Array.isArray(value) && value.length <= 16 && value.every((entry) => typeof entry === "string");
|
|
1628
|
+
}
|
|
1629
|
+
function safeParams(params) {
|
|
1630
|
+
if (!params) return void 0;
|
|
1631
|
+
const out = {};
|
|
1632
|
+
for (const key of PARAM_KEYS) {
|
|
1633
|
+
const value = params[key];
|
|
1634
|
+
if (value !== void 0 && isParamValue(value)) out[key] = Array.isArray(value) ? [...value] : value;
|
|
1635
|
+
}
|
|
1636
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
1637
|
+
}
|
|
1638
|
+
var BINDING_FIELDS = [
|
|
1639
|
+
"provider",
|
|
1640
|
+
"baseUrl",
|
|
1641
|
+
"model",
|
|
1642
|
+
"apiKey",
|
|
1643
|
+
"preset"
|
|
1644
|
+
];
|
|
1645
|
+
function isRecord(value) {
|
|
1646
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1647
|
+
}
|
|
1648
|
+
function text(value) {
|
|
1649
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
1650
|
+
}
|
|
1651
|
+
function hasBinding(binding) {
|
|
1652
|
+
return !!binding && (!!binding.provider || !!binding.preset);
|
|
1653
|
+
}
|
|
1654
|
+
function parseBinding(value, path, issues) {
|
|
1655
|
+
if (value === void 0) return void 0;
|
|
1656
|
+
if (!isRecord(value)) {
|
|
1657
|
+
issues.push(`${path} must be an object`);
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
const binding = {};
|
|
1661
|
+
for (const field of BINDING_FIELDS) {
|
|
1662
|
+
const raw = value[field];
|
|
1663
|
+
if (raw === void 0 || raw === null || raw === "") continue;
|
|
1664
|
+
const parsed = text(raw);
|
|
1665
|
+
if (!parsed) {
|
|
1666
|
+
issues.push(`${path}.${field} must be a non-empty string`);
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1669
|
+
binding[field] = parsed;
|
|
1670
|
+
}
|
|
1671
|
+
if (value.params !== void 0) if (!isRecord(value.params)) issues.push(`${path}.params must be an object`);
|
|
1672
|
+
else {
|
|
1673
|
+
for (const [key, param] of Object.entries(value.params)) if (!PARAM_KEYS.includes(key)) issues.push(`${path}.params.${key} is not a supported generation option (${INFERENCE_PROVIDER_PARAM_KEYS.join(", ")})`);
|
|
1674
|
+
else if (!isParamValue(param)) issues.push(`${path}.params.${key} must be a number or string`);
|
|
1675
|
+
const params = safeParams(value.params);
|
|
1676
|
+
if (params) binding.params = params;
|
|
1677
|
+
}
|
|
1678
|
+
if (binding.preset) {
|
|
1679
|
+
if (!isInferencePresetId(binding.preset)) issues.push(`${path}.preset is not a known preset`);
|
|
1680
|
+
else {
|
|
1681
|
+
if (!binding.model) issues.push(`${path}.model is required with preset`);
|
|
1682
|
+
if (INFERENCE_PRESETS[binding.preset].requiresApiKey && !binding.apiKey) issues.push(`${path}.apiKey is required by preset`);
|
|
1683
|
+
}
|
|
1684
|
+
if (binding.provider) issues.push(`${path} names both provider and preset`);
|
|
1685
|
+
} else if (Object.keys(binding).length > 0 && !binding.provider) issues.push(`${path}.provider is required`);
|
|
1686
|
+
if (binding.baseUrl && !isPlainEndpoint(binding.baseUrl)) issues.push(`${path}.baseUrl must be an http(s) URL without credentials, query or fragment`);
|
|
1687
|
+
if (binding.provider && !binding.preset) {
|
|
1688
|
+
if (!binding.baseUrl) issues.push(`${path}.baseUrl is required (a stored binding names its own endpoint)`);
|
|
1689
|
+
if (!binding.apiKey) issues.push(`${path}.apiKey is required (a placeholder such as 'smrt-local' for a keyless local server)`);
|
|
1690
|
+
}
|
|
1691
|
+
return binding;
|
|
1692
|
+
}
|
|
1693
|
+
function parseInferenceProviderConfig(value) {
|
|
1694
|
+
let raw = value;
|
|
1695
|
+
if (typeof raw === "string") try {
|
|
1696
|
+
raw = JSON.parse(raw);
|
|
1697
|
+
} catch {
|
|
1698
|
+
throw new InferenceProviderConfigError(["value is not JSON"]);
|
|
1699
|
+
}
|
|
1700
|
+
if (!isRecord(raw)) throw new InferenceProviderConfigError(["value must be an object"]);
|
|
1701
|
+
const issues = [];
|
|
1702
|
+
const config = { ...parseBinding(Object.fromEntries(Object.entries(raw).filter(([key]) => key !== "tiers" && key !== "source" && key !== "version" && key !== "mode" && key !== "gatewayKeyAlias")), "config", issues) ?? {} };
|
|
1703
|
+
if (raw.version !== void 0 && raw.version !== 1) issues.push("version must be 1");
|
|
1704
|
+
else if (raw.version === 1) config.version = 1;
|
|
1705
|
+
if (raw.source !== void 0) if (INFERENCE_PROVIDER_CONFIG_SOURCES.includes(raw.source)) config.source = raw.source;
|
|
1706
|
+
else issues.push(`source must be one of ${INFERENCE_PROVIDER_CONFIG_SOURCES.join(", ")}`);
|
|
1707
|
+
if (raw.mode !== void 0) if (isInferenceMode(raw.mode)) config.mode = raw.mode;
|
|
1708
|
+
else issues.push(`mode must be one of ${INFERENCE_MODES.join(", ")}`);
|
|
1709
|
+
if (raw.gatewayKeyAlias !== void 0) {
|
|
1710
|
+
const alias = text(raw.gatewayKeyAlias);
|
|
1711
|
+
if (!alias || alias.length > 200) issues.push("gatewayKeyAlias must be a non-empty string of at most 200 characters");
|
|
1712
|
+
else if (config.source !== "hosted") issues.push("gatewayKeyAlias needs source 'hosted'");
|
|
1713
|
+
else config.gatewayKeyAlias = alias;
|
|
1714
|
+
}
|
|
1715
|
+
if (raw.tiers !== void 0) if (!isRecord(raw.tiers)) issues.push("tiers must be an object");
|
|
1716
|
+
else {
|
|
1717
|
+
const tiers = {};
|
|
1718
|
+
for (const [tier, binding] of Object.entries(raw.tiers)) {
|
|
1719
|
+
if (!isInferenceTier(tier)) {
|
|
1720
|
+
issues.push(`tiers.${tier} is not a tier (${INFERENCE_TIERS.join(", ")})`);
|
|
1721
|
+
continue;
|
|
1722
|
+
}
|
|
1723
|
+
const parsed = parseBinding(binding, `tiers.${tier}`, issues);
|
|
1724
|
+
if (parsed && hasBinding(parsed)) tiers[tier] = parsed;
|
|
1725
|
+
}
|
|
1726
|
+
if (Object.keys(tiers).length > 0) config.tiers = tiers;
|
|
1727
|
+
}
|
|
1728
|
+
for (const known of Object.keys(raw)) if (![
|
|
1729
|
+
...BINDING_FIELDS,
|
|
1730
|
+
"params",
|
|
1731
|
+
"tiers",
|
|
1732
|
+
"source",
|
|
1733
|
+
"version",
|
|
1734
|
+
"mode",
|
|
1735
|
+
"gatewayKeyAlias"
|
|
1736
|
+
].includes(known)) issues.push(`config.${known} is not a provider config field`);
|
|
1737
|
+
if (issues.length > 0) throw new InferenceProviderConfigError(issues);
|
|
1738
|
+
return config;
|
|
1739
|
+
}
|
|
1740
|
+
function isPlainEndpoint(baseUrl) {
|
|
1741
|
+
try {
|
|
1742
|
+
const url = new URL(baseUrl);
|
|
1743
|
+
return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password && !url.search && !url.hash;
|
|
1744
|
+
} catch {
|
|
1745
|
+
return false;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
function isLoopbackUrl(baseUrl) {
|
|
1749
|
+
if (!baseUrl) return false;
|
|
1750
|
+
try {
|
|
1751
|
+
const host = new URL(baseUrl).hostname.toLowerCase();
|
|
1752
|
+
return host === "localhost" || host.endsWith(".localhost") || host === "[::1]" || host === "::1" || host === "0.0.0.0" || /^127\.\d+\.\d+\.\d+$/.test(host);
|
|
1753
|
+
} catch {
|
|
1754
|
+
return false;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
function inferenceProviderSource(config, binding) {
|
|
1758
|
+
if (config.source) return config.source;
|
|
1759
|
+
if (binding.preset && isInferencePresetId(binding.preset)) return INFERENCE_PRESETS[binding.preset].requiresApiKey ? "byo" : "local";
|
|
1760
|
+
if (binding.provider?.toLowerCase() === "ollama") return "local";
|
|
1761
|
+
return isLoopbackUrl(binding.baseUrl) ? "local" : "byo";
|
|
1762
|
+
}
|
|
1763
|
+
var KEYLESS_PLACEHOLDER = "smrt-local";
|
|
1764
|
+
function bindingToTarget(config, binding) {
|
|
1765
|
+
const source = inferenceProviderSource(config, binding);
|
|
1766
|
+
const usageTags = config.gatewayKeyAlias ? { usageTags: { gatewayKeyAlias: config.gatewayKeyAlias } } : {};
|
|
1767
|
+
if (binding.preset && isInferencePresetId(binding.preset)) return {
|
|
1768
|
+
provider: "openai",
|
|
1769
|
+
baseUrl: binding.baseUrl ?? INFERENCE_PRESETS[binding.preset].baseUrl,
|
|
1770
|
+
...binding.model ? { model: binding.model } : {},
|
|
1771
|
+
apiKey: binding.apiKey ?? KEYLESS_PLACEHOLDER,
|
|
1772
|
+
...safeParams(binding.params) ? { params: safeParams(binding.params) } : {},
|
|
1773
|
+
source,
|
|
1774
|
+
...usageTags
|
|
1775
|
+
};
|
|
1776
|
+
return {
|
|
1777
|
+
...binding.provider ? { provider: binding.provider } : {},
|
|
1778
|
+
...binding.model ? { model: binding.model } : {},
|
|
1779
|
+
...binding.baseUrl ? { baseUrl: binding.baseUrl } : {},
|
|
1780
|
+
...binding.apiKey ? { apiKey: binding.apiKey } : {},
|
|
1781
|
+
...safeParams(binding.params) ? { params: safeParams(binding.params) } : {},
|
|
1782
|
+
source,
|
|
1783
|
+
...usageTags
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
function inferenceProviderBindingFor(config, tier) {
|
|
1787
|
+
if (!config) return void 0;
|
|
1788
|
+
const own = config.tiers?.[tier];
|
|
1789
|
+
if (hasBinding(own)) return own;
|
|
1790
|
+
return hasBinding(config) ? config : void 0;
|
|
1791
|
+
}
|
|
1792
|
+
function inferenceProviderTargets(config) {
|
|
1793
|
+
const targets = {};
|
|
1794
|
+
if (!config) return targets;
|
|
1795
|
+
for (const tier of INFERENCE_TIERS) {
|
|
1796
|
+
const binding = inferenceProviderBindingFor(config, tier);
|
|
1797
|
+
if (binding) targets[tier] = bindingToTarget(config, binding);
|
|
1798
|
+
}
|
|
1799
|
+
return targets;
|
|
1800
|
+
}
|
|
1801
|
+
function redactBinding(binding) {
|
|
1802
|
+
const params = safeParams(binding.params);
|
|
1803
|
+
return {
|
|
1804
|
+
...binding.provider ? { provider: binding.provider } : {},
|
|
1805
|
+
...binding.preset ? { preset: binding.preset } : {},
|
|
1806
|
+
...binding.baseUrl ? { baseUrl: binding.baseUrl } : {},
|
|
1807
|
+
...binding.model ? { model: binding.model } : {},
|
|
1808
|
+
...params ? { params } : {},
|
|
1809
|
+
hasApiKey: !!binding.apiKey && binding.apiKey !== KEYLESS_PLACEHOLDER
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
function redactInferenceProviderConfig(config) {
|
|
1813
|
+
const { tiers, version, source, mode, gatewayKeyAlias, ...binding } = config;
|
|
1814
|
+
const redacted = {
|
|
1815
|
+
...redactBinding(binding),
|
|
1816
|
+
...version ? { version } : {},
|
|
1817
|
+
...source ? { source } : {},
|
|
1818
|
+
...mode ? { mode } : {},
|
|
1819
|
+
...gatewayKeyAlias ? { gatewayKeyAlias } : {}
|
|
1820
|
+
};
|
|
1821
|
+
if (tiers) {
|
|
1822
|
+
redacted.tiers = {};
|
|
1823
|
+
for (const [tier, value] of Object.entries(tiers)) if (value) redacted.tiers[tier] = redactBinding(value);
|
|
1824
|
+
}
|
|
1825
|
+
return redacted;
|
|
1826
|
+
}
|
|
1827
|
+
function redactInferenceTarget(target) {
|
|
1828
|
+
const params = safeParams(target.params);
|
|
1829
|
+
return {
|
|
1830
|
+
...target.provider ? { provider: target.provider } : {},
|
|
1831
|
+
...target.baseUrl ? { baseUrl: target.baseUrl } : {},
|
|
1832
|
+
...target.model ? { model: target.model } : {},
|
|
1833
|
+
...target.latency ? { latency: target.latency } : {},
|
|
1834
|
+
...target.source ? { source: target.source } : {},
|
|
1835
|
+
...params ? { params } : {},
|
|
1836
|
+
...target.usageTags ? { usageTags: { ...target.usageTags } } : {},
|
|
1837
|
+
hasApiKey: !!target.apiKey && target.apiKey !== KEYLESS_PLACEHOLDER
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
function inferenceProviderConfigSecretName(scope) {
|
|
1841
|
+
const userId = text(scope.userId);
|
|
1842
|
+
return userId ? `${INFERENCE_PROVIDER_CONFIG_SECRET_TYPE}:user:${userId}` : `${INFERENCE_PROVIDER_CONFIG_SECRET_TYPE}:tenant`;
|
|
1843
|
+
}
|
|
1844
|
+
//#endregion
|
|
1845
|
+
export { candidateLabel as A, InferenceExhaustedError as B, createChainClient as C, EXECUTOR_KINDS as D, CHAIN_KINDS as E, findInferenceConfigIssues as F, hasInferenceChains as I, validateInferenceConfig as L, expandTarget as M, findFusionDefinitionIssues as N, FUSION_GATE_CONTINUE as O, findFusionPromptIssues as P, validateInferenceTarget as R, setFusionPromptResolver as S, isInferenceAbort as T, InferenceFusionError as V, createFusionClient as _, inferenceProviderBindingFor as a, listFusions as b, inferenceProviderTargets as c, redactInferenceTarget as d, createInferenceResolver as f, createChainExecutor as g, clearFusions as h, InferenceProviderConfigError as i, candidateToStep as j, FUSION_SEAT_COUNTS as k, parseInferenceProviderConfig as l, registerInference as m, INFERENCE_PROVIDER_CONFIG_SOURCES as n, inferenceProviderConfigSecretName as o, isInferenceRegistered as p, INFERENCE_PROVIDER_PARAM_KEYS as r, inferenceProviderSource as s, INFERENCE_PROVIDER_CONFIG_SECRET_TYPE as t, redactInferenceProviderConfig as u, defineFusion as v, classifyInferenceFailure as w, runFusion as x, getFusion as y, InferenceConfigError as z };
|
|
1846
|
+
|
|
1847
|
+
//# sourceMappingURL=provider-config-4Bx8N52X.js.map
|