@independo/inderun-web 0.2.0-dev.1 → 0.2.0-dev.3
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/README.md +117 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +3 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/onnx-provider.d.ts +78 -0
- package/dist/onnx-provider.d.ts.map +1 -0
- package/dist/onnx-provider.js +217 -0
- package/dist/onnx-runtime.d.ts +110 -0
- package/dist/onnx-runtime.d.ts.map +1 -0
- package/dist/onnx-runtime.js +76 -0
- package/dist/onnx-transformers-runtime.d.ts +72 -0
- package/dist/onnx-transformers-runtime.d.ts.map +1 -0
- package/dist/onnx-transformers-runtime.js +210 -0
- package/dist/onnx.d.ts +13 -0
- package/dist/onnx.d.ts.map +1 -0
- package/dist/onnx.js +12 -0
- package/dist/route-planner.d.ts +31 -8
- package/dist/route-planner.d.ts.map +1 -1
- package/dist/route-planner.js +46 -17
- package/dist/router.d.ts +11 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +26 -20
- package/dist/web-sdk.d.ts +12 -3
- package/dist/web-sdk.d.ts.map +1 -1
- package/dist/web-sdk.js +20 -8
- package/package.json +15 -3
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ModelPackage } from "@independo/inderun-contracts";
|
|
2
|
+
import { type OnnxTextGenerationRuntime } from "./onnx-runtime.js";
|
|
3
|
+
/**
|
|
4
|
+
* Structural view of the `@huggingface/transformers` entry points this runtime uses.
|
|
5
|
+
*
|
|
6
|
+
* The dependency is optional, so the module is typed structurally instead of imported for types.
|
|
7
|
+
*/
|
|
8
|
+
export interface TransformersModule {
|
|
9
|
+
/** Builds a task pipeline, downloading or loading model assets as needed. */
|
|
10
|
+
pipeline: (task: string, model?: string, options?: Record<string, unknown>) => Promise<TransformersTextGenerator>;
|
|
11
|
+
/** Global Transformers.js environment used to point the loader at local model assets. */
|
|
12
|
+
env?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Structural view of a Transformers.js `text-generation` pipeline.
|
|
16
|
+
*/
|
|
17
|
+
export type TransformersTextGenerator = (input: unknown, options?: Record<string, unknown>) => Promise<unknown>;
|
|
18
|
+
/**
|
|
19
|
+
* Configuration for the Transformers.js-backed ONNX runtime.
|
|
20
|
+
*/
|
|
21
|
+
export interface TransformersJsRuntimeOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Overrides how the `@huggingface/transformers` module is loaded. Tests inject a fake module;
|
|
24
|
+
* production uses a lazy dynamic import so the dependency stays optional.
|
|
25
|
+
*/
|
|
26
|
+
load?: () => Promise<TransformersModule>;
|
|
27
|
+
/**
|
|
28
|
+
* Execution backend passed to Transformers.js. Defaults to `webgpu` when `navigator.gpu`
|
|
29
|
+
* exists and `wasm` otherwise. This is an ONNX Runtime execution backend, not an IndeRun
|
|
30
|
+
* routing concept.
|
|
31
|
+
*/
|
|
32
|
+
device?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Weight quantization passed to Transformers.js (for example `q4`, `q4f16`, `q8`, `fp16`,
|
|
35
|
+
* `fp32`).
|
|
36
|
+
*
|
|
37
|
+
* Defaults to a quantized variant (`q4f16` on WebGPU, `q4` otherwise) because Transformers.js
|
|
38
|
+
* would otherwise load full `fp32` weights, which exhausts browser memory for all but the
|
|
39
|
+
* smallest models. Set explicitly when the model does not publish the default variant.
|
|
40
|
+
*/
|
|
41
|
+
dtype?: string;
|
|
42
|
+
/** Base path or URL for locally served model assets (`bundled` / `app_managed` sources). */
|
|
43
|
+
localModelPath?: string;
|
|
44
|
+
/** Extra options merged into the Transformers.js `pipeline()` call. */
|
|
45
|
+
pipelineOptions?: Record<string, unknown>;
|
|
46
|
+
/**
|
|
47
|
+
* Supplies a pre-built generator instead of letting this runtime construct one. Required for
|
|
48
|
+
* `programmatic` model sources, where the application owns model resolution.
|
|
49
|
+
*/
|
|
50
|
+
createGenerator?: (context: {
|
|
51
|
+
modelPackage: ModelPackage;
|
|
52
|
+
module: TransformersModule;
|
|
53
|
+
device: string;
|
|
54
|
+
}) => Promise<TransformersTextGenerator>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Creates the default Web ONNX runtime, backed by Transformers.js (which runs ONNX models through
|
|
58
|
+
* `onnxruntime-web`).
|
|
59
|
+
*
|
|
60
|
+
* ONNX Runtime GenAI has no browser build, so Transformers.js fills the generative-loop role
|
|
61
|
+
* (tokenization, sampling, KV cache) behind the `OnnxTextGenerationRuntime` seam. Applications that
|
|
62
|
+
* need a different runtime implement the seam themselves and pass it to the provider.
|
|
63
|
+
*
|
|
64
|
+
* `@huggingface/transformers` is an optional dependency the application installs: when it is absent
|
|
65
|
+
* the runtime
|
|
66
|
+
* reports itself unavailable instead of throwing, so routing degrades to an explainable
|
|
67
|
+
* `capability_unavailable` rejection.
|
|
68
|
+
*
|
|
69
|
+
* @param options - Loader, backend, and model-resolution overrides.
|
|
70
|
+
*/
|
|
71
|
+
export declare function createTransformersJsRuntime(options?: TransformersJsRuntimeOptions): OnnxTextGenerationRuntime;
|
|
72
|
+
//# sourceMappingURL=onnx-transformers-runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"onnx-transformers-runtime.d.ts","sourceRoot":"","sources":["../src/onnx-transformers-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAKL,KAAK,yBAAyB,EAC/B,MAAM,mBAAmB,CAAC;AAE3B;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,6EAA6E;IAC7E,QAAQ,EAAE,CACR,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC9B,OAAO,CAAC,yBAAyB,CAAC,CAAC;IACxC,yFAAyF;IACzF,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG,CACtC,KAAK,EAAE,OAAO,EACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC9B,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtB;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC3C;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACzC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4FAA4F;IAC5F,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uEAAuE;IACvE,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE;QAC1B,YAAY,EAAE,YAAY,CAAC;QAC3B,MAAM,EAAE,kBAAkB,CAAC;QAC3B,MAAM,EAAE,MAAM,CAAC;KAChB,KAAK,OAAO,CAAC,yBAAyB,CAAC,CAAC;CAC1C;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,GAAE,4BAAiC,GACzC,yBAAyB,CAyI3B"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { OnnxRuntimeError } from "./onnx-runtime.js";
|
|
2
|
+
const TEXT_GENERATION_TASK = "text-generation";
|
|
3
|
+
/**
|
|
4
|
+
* Creates the default Web ONNX runtime, backed by Transformers.js (which runs ONNX models through
|
|
5
|
+
* `onnxruntime-web`).
|
|
6
|
+
*
|
|
7
|
+
* ONNX Runtime GenAI has no browser build, so Transformers.js fills the generative-loop role
|
|
8
|
+
* (tokenization, sampling, KV cache) behind the `OnnxTextGenerationRuntime` seam. Applications that
|
|
9
|
+
* need a different runtime implement the seam themselves and pass it to the provider.
|
|
10
|
+
*
|
|
11
|
+
* `@huggingface/transformers` is an optional dependency the application installs: when it is absent
|
|
12
|
+
* the runtime
|
|
13
|
+
* reports itself unavailable instead of throwing, so routing degrades to an explainable
|
|
14
|
+
* `capability_unavailable` rejection.
|
|
15
|
+
*
|
|
16
|
+
* @param options - Loader, backend, and model-resolution overrides.
|
|
17
|
+
*/
|
|
18
|
+
export function createTransformersJsRuntime(options = {}) {
|
|
19
|
+
let modulePromise;
|
|
20
|
+
let generatorPromise;
|
|
21
|
+
async function loadModule() {
|
|
22
|
+
modulePromise ??= (options.load ?? loadTransformersModule)();
|
|
23
|
+
return modulePromise;
|
|
24
|
+
}
|
|
25
|
+
async function loadGenerator(modelPackage) {
|
|
26
|
+
generatorPromise ??= (async () => {
|
|
27
|
+
const module = await loadModule();
|
|
28
|
+
const device = resolveDevice(options.device);
|
|
29
|
+
if (options.createGenerator) {
|
|
30
|
+
return options.createGenerator({ modelPackage, module, device });
|
|
31
|
+
}
|
|
32
|
+
const modelRef = getModelRef(modelPackage);
|
|
33
|
+
if (!modelRef) {
|
|
34
|
+
throw new OnnxRuntimeError("capability", "model source unavailable: the model package does not declare source.ref.");
|
|
35
|
+
}
|
|
36
|
+
applyEnvironment(module, modelPackage, options.localModelPath);
|
|
37
|
+
const pipelineOptions = {
|
|
38
|
+
device,
|
|
39
|
+
dtype: options.dtype ?? defaultDtype(device),
|
|
40
|
+
...(options.pipelineOptions ?? {})
|
|
41
|
+
};
|
|
42
|
+
return module.pipeline(TEXT_GENERATION_TASK, modelRef, pipelineOptions);
|
|
43
|
+
})();
|
|
44
|
+
return generatorPromise;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
async prepare(modelPackage) {
|
|
48
|
+
let module;
|
|
49
|
+
try {
|
|
50
|
+
module = await loadModule();
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
modulePromise = undefined;
|
|
54
|
+
return {
|
|
55
|
+
available: false,
|
|
56
|
+
reason: `runtime package unavailable: install the optional dependency @huggingface/transformers (${getErrorMessage(err)}).`
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (typeof module.pipeline !== "function") {
|
|
60
|
+
return {
|
|
61
|
+
available: false,
|
|
62
|
+
reason: "runtime initialization failed: @huggingface/transformers did not expose pipeline()."
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (modelPackage.format !== "onnx") {
|
|
66
|
+
return {
|
|
67
|
+
available: false,
|
|
68
|
+
reason: `unsupported model format: the Transformers.js runtime consumes ONNX weights, received '${modelPackage.format}'.`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const sourceType = modelPackage.source?.sourceType;
|
|
72
|
+
if (sourceType === "programmatic" && !options.createGenerator) {
|
|
73
|
+
return {
|
|
74
|
+
available: false,
|
|
75
|
+
reason: "model source unavailable: programmatic model packages require a createGenerator option."
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (!options.createGenerator && !getModelRef(modelPackage)) {
|
|
79
|
+
return {
|
|
80
|
+
available: false,
|
|
81
|
+
reason: "model source unavailable: the model package does not declare source.ref."
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return { available: true };
|
|
85
|
+
},
|
|
86
|
+
async generate(input, signal) {
|
|
87
|
+
let generator;
|
|
88
|
+
try {
|
|
89
|
+
generator = await loadGenerator(input.modelPackage);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
generatorPromise = undefined;
|
|
93
|
+
if (err instanceof OnnxRuntimeError)
|
|
94
|
+
throw err;
|
|
95
|
+
if (isResourceExhaustion(err)) {
|
|
96
|
+
throw new OnnxRuntimeError("unavailable", `insufficient memory to load the model: ${getErrorMessage(err)}. Try a smaller model or a more aggressive dtype (for example 'q4').`, err);
|
|
97
|
+
}
|
|
98
|
+
throw new OnnxRuntimeError("capability", `model files missing or unreadable: ${getErrorMessage(err)}`, err);
|
|
99
|
+
}
|
|
100
|
+
if (signal?.aborted) {
|
|
101
|
+
throw new OnnxRuntimeError("timeout", "ONNX generation was aborted before it started.");
|
|
102
|
+
}
|
|
103
|
+
let raw;
|
|
104
|
+
try {
|
|
105
|
+
raw = await generator(input.messages, createGenerationOptions(input, signal));
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
throw new OnnxRuntimeError("internal", `ONNX generation failed: ${getErrorMessage(err)}`, err);
|
|
109
|
+
}
|
|
110
|
+
const text = extractGeneratedText(raw);
|
|
111
|
+
if (text === undefined) {
|
|
112
|
+
throw new OnnxRuntimeError("internal", "model output malformed: the Transformers.js pipeline returned no text.");
|
|
113
|
+
}
|
|
114
|
+
return { text };
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async function loadTransformersModule() {
|
|
119
|
+
// The specifier stays a literal so bundlers and dev servers resolve the bare package name; the
|
|
120
|
+
// `as string` cast keeps TypeScript from requiring the optional dependency's types here. Apps that
|
|
121
|
+
// do not install Transformers.js should pass their own `load` (or a different runtime) rather than
|
|
122
|
+
// relying on this default, which their bundler will fail to resolve.
|
|
123
|
+
return (await import("@huggingface/transformers"));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Browser-viable weight variant. Transformers.js defaults to `fp32`, which makes ONNX Runtime
|
|
127
|
+
* allocate the full-precision model and fail session creation with an allocation error for
|
|
128
|
+
* anything but tiny models.
|
|
129
|
+
*/
|
|
130
|
+
function defaultDtype(device) {
|
|
131
|
+
return device === "webgpu" ? "q4f16" : "q4";
|
|
132
|
+
}
|
|
133
|
+
function resolveDevice(configured) {
|
|
134
|
+
if (configured !== undefined)
|
|
135
|
+
return configured;
|
|
136
|
+
const gpu = globalThis.navigator?.gpu;
|
|
137
|
+
return gpu ? "webgpu" : "wasm";
|
|
138
|
+
}
|
|
139
|
+
function getModelRef(modelPackage) {
|
|
140
|
+
const ref = modelPackage.source?.ref;
|
|
141
|
+
return typeof ref === "string" && ref.length > 0 ? ref : undefined;
|
|
142
|
+
}
|
|
143
|
+
function applyEnvironment(module, modelPackage, localModelPath) {
|
|
144
|
+
const env = module.env;
|
|
145
|
+
if (!env)
|
|
146
|
+
return;
|
|
147
|
+
const sourceType = modelPackage.source?.sourceType;
|
|
148
|
+
if (sourceType === "bundled" || sourceType === "app_managed") {
|
|
149
|
+
env.allowLocalModels = true;
|
|
150
|
+
env.allowRemoteModels = false;
|
|
151
|
+
if (localModelPath !== undefined) {
|
|
152
|
+
env.localModelPath = localModelPath;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function createGenerationOptions(input, signal) {
|
|
157
|
+
const generation = input.generation;
|
|
158
|
+
const generationOptions = {};
|
|
159
|
+
if (generation?.maxOutputTokens !== undefined) {
|
|
160
|
+
generationOptions.max_new_tokens = generation.maxOutputTokens;
|
|
161
|
+
}
|
|
162
|
+
if (generation?.temperature !== undefined) {
|
|
163
|
+
generationOptions.temperature = generation.temperature;
|
|
164
|
+
generationOptions.do_sample = generation.temperature > 0;
|
|
165
|
+
}
|
|
166
|
+
if (generation?.topP !== undefined) {
|
|
167
|
+
generationOptions.top_p = generation.topP;
|
|
168
|
+
}
|
|
169
|
+
if (generation?.stop !== undefined && generation.stop.length > 0) {
|
|
170
|
+
generationOptions.stop_strings = generation.stop;
|
|
171
|
+
}
|
|
172
|
+
if (signal !== undefined) {
|
|
173
|
+
generationOptions.signal = signal;
|
|
174
|
+
}
|
|
175
|
+
return generationOptions;
|
|
176
|
+
}
|
|
177
|
+
function extractGeneratedText(raw) {
|
|
178
|
+
const first = Array.isArray(raw) ? raw[0] : raw;
|
|
179
|
+
if (!isRecord(first)) {
|
|
180
|
+
return typeof first === "string" ? first : undefined;
|
|
181
|
+
}
|
|
182
|
+
const generated = first.generated_text;
|
|
183
|
+
if (typeof generated === "string") {
|
|
184
|
+
return generated;
|
|
185
|
+
}
|
|
186
|
+
if (Array.isArray(generated)) {
|
|
187
|
+
const last = generated[generated.length - 1];
|
|
188
|
+
if (isRecord(last) && typeof last.content === "string") {
|
|
189
|
+
return last.content;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Detects out-of-memory failures surfaced by ONNX Runtime session creation, which arrive as
|
|
196
|
+
* allocation errors from the WASM/native layer rather than as typed errors.
|
|
197
|
+
*/
|
|
198
|
+
function isResourceExhaustion(error) {
|
|
199
|
+
const message = getErrorMessage(error).toLowerCase();
|
|
200
|
+
return (message.includes("bad_alloc") ||
|
|
201
|
+
message.includes("out of memory") ||
|
|
202
|
+
message.includes("failed to allocate") ||
|
|
203
|
+
message.includes("allocation failed"));
|
|
204
|
+
}
|
|
205
|
+
function isRecord(value) {
|
|
206
|
+
return typeof value === "object" && value !== null;
|
|
207
|
+
}
|
|
208
|
+
function getErrorMessage(error) {
|
|
209
|
+
return error instanceof Error ? error.message : String(error);
|
|
210
|
+
}
|
package/dist/onnx.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-specific entry point for the Web member of the ONNX Runtime provider family.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of the unified SDK index (`@independo/inderun-web`) so the top-level
|
|
5
|
+
* surface stays provider-agnostic. Import from `@independo/inderun-web/onnx`
|
|
6
|
+
* when registering the adapter manually or when implementing a custom
|
|
7
|
+
* `OnnxTextGenerationRuntime`; most browser apps should pass `onnx` to
|
|
8
|
+
* `createIndeRunWeb` from the main entry point instead.
|
|
9
|
+
*/
|
|
10
|
+
export { OnnxRuntimeWebProvider, DEFAULT_ONNX_WEB_PROVIDER_ID, SUPPORTED_WEB_MODEL_SOURCE_TYPES, type OnnxProviderOptions } from "./onnx-provider.js";
|
|
11
|
+
export { OnnxRuntimeError, createFixtureOnnxRuntime, type FixtureOnnxRuntimeOptions, type OnnxGenerationInput, type OnnxGenerationMessage, type OnnxGenerationOutput, type OnnxRuntimeAvailability, type OnnxRuntimeErrorKind, type OnnxTextGenerationRuntime } from "./onnx-runtime.js";
|
|
12
|
+
export { createTransformersJsRuntime, type TransformersJsRuntimeOptions, type TransformersModule, type TransformersTextGenerator } from "./onnx-transformers-runtime.js";
|
|
13
|
+
//# sourceMappingURL=onnx.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"onnx.d.ts","sourceRoot":"","sources":["../src/onnx.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EACL,sBAAsB,EACtB,4BAA4B,EAC5B,gCAAgC,EAChC,KAAK,mBAAmB,EACzB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,gBAAgB,EAChB,wBAAwB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC/B,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,2BAA2B,EAC3B,KAAK,4BAA4B,EACjC,KAAK,kBAAkB,EACvB,KAAK,yBAAyB,EAC/B,MAAM,gCAAgC,CAAC"}
|
package/dist/onnx.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-specific entry point for the Web member of the ONNX Runtime provider family.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of the unified SDK index (`@independo/inderun-web`) so the top-level
|
|
5
|
+
* surface stays provider-agnostic. Import from `@independo/inderun-web/onnx`
|
|
6
|
+
* when registering the adapter manually or when implementing a custom
|
|
7
|
+
* `OnnxTextGenerationRuntime`; most browser apps should pass `onnx` to
|
|
8
|
+
* `createIndeRunWeb` from the main entry point instead.
|
|
9
|
+
*/
|
|
10
|
+
export { OnnxRuntimeWebProvider, DEFAULT_ONNX_WEB_PROVIDER_ID, SUPPORTED_WEB_MODEL_SOURCE_TYPES } from "./onnx-provider.js";
|
|
11
|
+
export { OnnxRuntimeError, createFixtureOnnxRuntime } from "./onnx-runtime.js";
|
|
12
|
+
export { createTransformersJsRuntime } from "./onnx-transformers-runtime.js";
|
package/dist/route-planner.d.ts
CHANGED
|
@@ -12,11 +12,28 @@ export interface SharedPlannerModule {
|
|
|
12
12
|
planRouteJson: (inputJson: string) => string | Promise<string>;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Coarse, privacy-safe reason the WASM planner produced no route plan. Used
|
|
16
|
+
* only for observability (telemetry/logging) — callers still fall back to
|
|
17
|
+
* local selection regardless of which reason fired.
|
|
18
|
+
*/
|
|
19
|
+
export type WasmUnavailableReason = "import_failed" | "invalid_module_shape" | "init_failed" | "plan_failed";
|
|
20
|
+
/**
|
|
21
|
+
* Result of a {@link RoutePlanner} attempt. `routePlan` is `null` when the
|
|
22
|
+
* planner could not produce a plan, in which case `source` is `"unavailable"`
|
|
23
|
+
* and `unavailableReason` narrows down why, so callers can log/emit telemetry
|
|
24
|
+
* instead of silently swapping planners.
|
|
25
|
+
*/
|
|
26
|
+
export interface PlannerOutcome {
|
|
27
|
+
routePlan: SharedPlannerRoutePlan | null;
|
|
28
|
+
source: "wasm" | "unavailable";
|
|
29
|
+
unavailableReason?: WasmUnavailableReason;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Strategy that turns planner input into a deterministic route plan, or an
|
|
33
|
+
* "unavailable" outcome (callers then fall back to local selection).
|
|
17
34
|
*/
|
|
18
35
|
export interface RoutePlanner {
|
|
19
|
-
planRoute(input: SharedPlannerInput): Promise<
|
|
36
|
+
planRoute(input: SharedPlannerInput): Promise<PlannerOutcome>;
|
|
20
37
|
}
|
|
21
38
|
/**
|
|
22
39
|
* A provider paired with the descriptor/capability projection the shared route
|
|
@@ -29,16 +46,22 @@ export interface ProviderRuntimeSnapshot {
|
|
|
29
46
|
}
|
|
30
47
|
/**
|
|
31
48
|
* {@link RoutePlanner} backed by the Rust route core compiled to WASM. The
|
|
32
|
-
* module is
|
|
33
|
-
*
|
|
49
|
+
* module specifier is a static literal (not configurable) so bundlers such as
|
|
50
|
+
* Vite/webpack/Rollup can statically resolve and chunk it — a variable
|
|
51
|
+
* specifier defeats bundler analysis and silently never loads in the browser
|
|
52
|
+
* (see issue #109). The module is imported lazily and memoized; if it cannot
|
|
53
|
+
* be loaded, `planRoute` resolves an "unavailable" outcome so the engine can
|
|
54
|
+
* fall back to local selection, and logs once via `console.warn` so the
|
|
55
|
+
* degradation is never fully silent.
|
|
34
56
|
*/
|
|
35
57
|
export declare class WasmRoutePlanner implements RoutePlanner {
|
|
36
|
-
private readonly moduleSpecifier;
|
|
37
58
|
private modulePromise?;
|
|
38
|
-
|
|
39
|
-
|
|
59
|
+
private loadFailureReason?;
|
|
60
|
+
private hasWarned;
|
|
61
|
+
planRoute(input: SharedPlannerInput): Promise<PlannerOutcome>;
|
|
40
62
|
private loadModule;
|
|
41
63
|
private importModule;
|
|
64
|
+
private warnOnce;
|
|
42
65
|
}
|
|
43
66
|
/**
|
|
44
67
|
* Describes each provider and evaluates its dynamic capabilities against the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-planner.d.ts","sourceRoot":"","sources":["../src/route-planner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,IAAI,sBAAsB,EACnC,iBAAiB,IAAI,kBAAkB,EACvC,WAAW,EACZ,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EACV,eAAe,EAGhB,MAAM,eAAe,CAAC;AAEvB,YAAY,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,CAAC;AAE3D;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAChE;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"route-planner.d.ts","sourceRoot":"","sources":["../src/route-planner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,IAAI,sBAAsB,EACnC,iBAAiB,IAAI,kBAAkB,EACvC,WAAW,EACZ,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EACV,eAAe,EAGhB,MAAM,eAAe,CAAC;AAEvB,YAAY,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,CAAC;AAE3D;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAChE;AAED;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAC/B,eAAe,GAAG,sBAAsB,GAAG,aAAa,GAAG,aAAa,CAAC;AAE3E;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,sBAAsB,GAAG,IAAI,CAAC;IACzC,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC;IAC/B,iBAAiB,CAAC,EAAE,qBAAqB,CAAC;CAC3C;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC/D;AAED;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,eAAe,CAAC;IAC1B,UAAU,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;IAClE,YAAY,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,CAAC;CACvE;AAED;;;;;;;;;GASG;AACH,qBAAa,gBAAiB,YAAW,YAAY;IACnD,OAAO,CAAC,aAAa,CAAC,CAAsC;IAC5D,OAAO,CAAC,iBAAiB,CAAC,CAAwB;IAClD,OAAO,CAAC,SAAS,CAAS;IAEpB,SAAS,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC;YAkBrD,UAAU;YAOV,YAAY;IA6B1B,OAAO,CAAC,QAAQ;CAUjB;AAED;;;GAGG;AACH,wBAAsB,+BAA+B,CACnD,iBAAiB,EAAE,eAAe,EAAE,EACpC,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAepC;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,uBAAuB,EAAE,EACpC,aAAa,EAAE,OAAO,GACrB,kBAAkB,CAsBpB"}
|
package/dist/route-planner.js
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
|
-
const DEFAULT_WASM_SPECIFIER = "@independo/inderun-route-core-wasm";
|
|
2
1
|
/**
|
|
3
2
|
* {@link RoutePlanner} backed by the Rust route core compiled to WASM. The
|
|
4
|
-
* module is
|
|
5
|
-
*
|
|
3
|
+
* module specifier is a static literal (not configurable) so bundlers such as
|
|
4
|
+
* Vite/webpack/Rollup can statically resolve and chunk it — a variable
|
|
5
|
+
* specifier defeats bundler analysis and silently never loads in the browser
|
|
6
|
+
* (see issue #109). The module is imported lazily and memoized; if it cannot
|
|
7
|
+
* be loaded, `planRoute` resolves an "unavailable" outcome so the engine can
|
|
8
|
+
* fall back to local selection, and logs once via `console.warn` so the
|
|
9
|
+
* degradation is never fully silent.
|
|
6
10
|
*/
|
|
7
11
|
export class WasmRoutePlanner {
|
|
8
|
-
moduleSpecifier;
|
|
9
12
|
modulePromise;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
+
loadFailureReason;
|
|
14
|
+
hasWarned = false;
|
|
13
15
|
async planRoute(input) {
|
|
14
16
|
const module = await this.loadModule();
|
|
15
17
|
if (!module) {
|
|
16
|
-
|
|
18
|
+
const unavailableReason = this.loadFailureReason ?? "import_failed";
|
|
19
|
+
this.warnOnce(unavailableReason);
|
|
20
|
+
return { routePlan: null, source: "unavailable", unavailableReason };
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const json = JSON.stringify(input);
|
|
24
|
+
const result = await module.planRouteJson(json);
|
|
25
|
+
return { routePlan: JSON.parse(result), source: "wasm" };
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
this.warnOnce("plan_failed", error);
|
|
29
|
+
return { routePlan: null, source: "unavailable", unavailableReason: "plan_failed" };
|
|
17
30
|
}
|
|
18
|
-
const json = JSON.stringify(input);
|
|
19
|
-
const result = await module.planRouteJson(json);
|
|
20
|
-
return JSON.parse(result);
|
|
21
31
|
}
|
|
22
32
|
async loadModule() {
|
|
23
33
|
if (!this.modulePromise) {
|
|
@@ -26,19 +36,38 @@ export class WasmRoutePlanner {
|
|
|
26
36
|
return this.modulePromise;
|
|
27
37
|
}
|
|
28
38
|
async importModule() {
|
|
39
|
+
let mod;
|
|
29
40
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
mod = (await import("@independo/inderun-route-core-wasm"));
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
this.loadFailureReason = "import_failed";
|
|
45
|
+
this.warnOnce("import_failed", error);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (typeof mod.planRouteJson !== "function") {
|
|
49
|
+
this.loadFailureReason = "invalid_module_shape";
|
|
50
|
+
this.warnOnce("invalid_module_shape");
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (mod.initSharedCore) {
|
|
54
|
+
try {
|
|
32
55
|
await mod.initSharedCore();
|
|
33
56
|
}
|
|
34
|
-
|
|
57
|
+
catch (error) {
|
|
58
|
+
this.loadFailureReason = "init_failed";
|
|
59
|
+
this.warnOnce("init_failed", error);
|
|
35
60
|
return null;
|
|
36
61
|
}
|
|
37
|
-
return mod;
|
|
38
62
|
}
|
|
39
|
-
|
|
40
|
-
|
|
63
|
+
return mod;
|
|
64
|
+
}
|
|
65
|
+
warnOnce(reason, error) {
|
|
66
|
+
if (this.hasWarned) {
|
|
67
|
+
return;
|
|
41
68
|
}
|
|
69
|
+
this.hasWarned = true;
|
|
70
|
+
console.warn(`[IndeRun] WASM route planner unavailable (${reason}); falling back to local route selection.`, error);
|
|
42
71
|
}
|
|
43
72
|
}
|
|
44
73
|
/**
|
package/dist/router.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { TaskRequest } from "@independo/inderun-contracts";
|
|
|
2
2
|
import type { HostServices } from "./host.js";
|
|
3
3
|
import type { ProviderAdapter } from "./provider.js";
|
|
4
4
|
import type { ProviderRegistry } from "./registry.js";
|
|
5
|
-
import { type RoutePlanner, type SharedPlannerRoutePlan } from "./route-planner.js";
|
|
5
|
+
import { type RoutePlanner, type SharedPlannerRoutePlan, type WasmUnavailableReason } from "./route-planner.js";
|
|
6
6
|
/**
|
|
7
7
|
* Output structure from the Router containing the chosen provider and a route selection description.
|
|
8
8
|
*/
|
|
@@ -23,6 +23,16 @@ export interface RouteSelection {
|
|
|
23
23
|
* Explanation detailing the selection decision. Useful for debugging and telemetry.
|
|
24
24
|
*/
|
|
25
25
|
explanation: string;
|
|
26
|
+
/**
|
|
27
|
+
* Which planner produced `routePlan`: the shared Rust/WASM core, or the
|
|
28
|
+
* local TypeScript fallback used when the WASM planner is unavailable.
|
|
29
|
+
*/
|
|
30
|
+
plannerSource: "wasm" | "fallback";
|
|
31
|
+
/**
|
|
32
|
+
* Set when `plannerSource` is `"fallback"` because the WASM planner failed;
|
|
33
|
+
* `undefined` when the fallback was not caused by a planner failure.
|
|
34
|
+
*/
|
|
35
|
+
plannerUnavailableReason?: WasmUnavailableReason;
|
|
26
36
|
}
|
|
27
37
|
/**
|
|
28
38
|
* Routing engine module. Filters registered providers against task types, request constraints,
|
package/dist/router.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAOtD,OAAO,EAIL,KAAK,YAAY,EAEjB,KAAK,sBAAsB,
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAOtD,OAAO,EAIL,KAAK,YAAY,EAEjB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAE3B,MAAM,oBAAoB,CAAC;AAE5B;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;OAEG;IACH,QAAQ,EAAE,eAAe,CAAC;IAC1B;;OAEG;IACH,iBAAiB,EAAE,eAAe,EAAE,CAAC;IACrC;;OAEG;IACH,SAAS,EAAE,sBAAsB,CAAC;IAClC;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,aAAa,EAAE,MAAM,GAAG,UAAU,CAAC;IACnC;;;OAGG;IACH,wBAAwB,CAAC,EAAE,qBAAqB,CAAC;CAClD;AAED;;;;GAIG;AACH,qBAAa,MAAM;IAEf,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,OAAO;gBADP,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,GAAE,YAAqC;IAGxD;;;;;;;;;OASG;IACG,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC;IAc5F,OAAO,CAAC,oBAAoB;IAW5B,OAAO,CAAC,mBAAmB;IAc3B,OAAO,CAAC,2BAA2B;IA2BnC,OAAO,CAAC,kBAAkB;IA0F1B,OAAO,CAAC,2BAA2B;IAiCnC,OAAO,CAAC,gBAAgB;CAYzB"}
|
package/dist/router.js
CHANGED
|
@@ -26,26 +26,26 @@ export class Router {
|
|
|
26
26
|
const online = await hostServices.connectivity.isOnline();
|
|
27
27
|
const snapshots = await collectProviderRuntimeSnapshots(this.registry.list(), hostServices);
|
|
28
28
|
const planInput = buildSharedPlannerInput(request, snapshots, online);
|
|
29
|
-
const
|
|
30
|
-
if (routePlan) {
|
|
31
|
-
return this.selectFromSharedPlan(snapshots, routePlan);
|
|
29
|
+
const outcome = await this.planner.planRoute(planInput);
|
|
30
|
+
if (outcome.routePlan) {
|
|
31
|
+
return this.selectFromSharedPlan(snapshots, outcome.routePlan);
|
|
32
32
|
}
|
|
33
|
-
return this.selectFallbackRoute(request, snapshots, online);
|
|
33
|
+
return this.selectFallbackRoute(request, snapshots, online, outcome.unavailableReason);
|
|
34
34
|
}
|
|
35
35
|
selectFromSharedPlan(snapshots, routePlan) {
|
|
36
36
|
if (!routePlan.selectedProviderId) {
|
|
37
37
|
throw this.routePlanFailure(routePlan);
|
|
38
38
|
}
|
|
39
|
-
return this.buildSelectionFromRoutePlan(snapshots, routePlan);
|
|
39
|
+
return this.buildSelectionFromRoutePlan(snapshots, routePlan, "wasm");
|
|
40
40
|
}
|
|
41
|
-
selectFallbackRoute(request, snapshots, online) {
|
|
41
|
+
selectFallbackRoute(request, snapshots, online, plannerUnavailableReason) {
|
|
42
42
|
const plan = this.createFallbackPlan(request, snapshots, online);
|
|
43
43
|
if (!plan.selectedProviderId) {
|
|
44
44
|
throw this.routePlanFailure(plan);
|
|
45
45
|
}
|
|
46
|
-
return this.buildSelectionFromRoutePlan(snapshots, plan);
|
|
46
|
+
return this.buildSelectionFromRoutePlan(snapshots, plan, "fallback", plannerUnavailableReason);
|
|
47
47
|
}
|
|
48
|
-
buildSelectionFromRoutePlan(snapshots, routePlan) {
|
|
48
|
+
buildSelectionFromRoutePlan(snapshots, routePlan, plannerSource, plannerUnavailableReason) {
|
|
49
49
|
const orderedProviders = routePlan.candidates
|
|
50
50
|
.map((candidate) => snapshots.find((snapshot) => snapshot.descriptor.id === candidate.providerId))
|
|
51
51
|
.filter((snapshot) => snapshot !== undefined);
|
|
@@ -57,7 +57,9 @@ export class Router {
|
|
|
57
57
|
provider: selected.provider,
|
|
58
58
|
fallbackProviders: orderedProviders.slice(1).map((snapshot) => snapshot.provider),
|
|
59
59
|
routePlan,
|
|
60
|
-
explanation: routePlan.explanation.summary
|
|
60
|
+
explanation: routePlan.explanation.summary,
|
|
61
|
+
plannerSource,
|
|
62
|
+
...(plannerUnavailableReason ? { plannerUnavailableReason } : {})
|
|
61
63
|
};
|
|
62
64
|
}
|
|
63
65
|
createFallbackPlan(request, snapshots, online) {
|
|
@@ -67,17 +69,24 @@ export class Router {
|
|
|
67
69
|
.filter((candidate) => candidate.descriptor.supports.run);
|
|
68
70
|
const localCandidates = eligible.filter((candidate) => candidate.descriptor.type !== "cloud");
|
|
69
71
|
const cloudCandidates = eligible.filter((candidate) => candidate.descriptor.type === "cloud");
|
|
70
|
-
|
|
72
|
+
// Constraint filtering must apply to the whole candidate list, not just the selected provider:
|
|
73
|
+
// the remainder becomes the engine's fallback chain, and a fallback that violates the request
|
|
74
|
+
// constraints would let a `local_required` run be retried against a cloud provider.
|
|
75
|
+
const admissible = eligible.filter((candidate) => {
|
|
71
76
|
const descriptor = candidate.descriptor;
|
|
72
77
|
const constraints = planInput.constraints;
|
|
73
|
-
|
|
78
|
+
// Mirrors `is_data_private` in the shared Rust planner: a provider is private when it
|
|
79
|
+
// declares that data does not leave the device, defaulting to non-cloud providers.
|
|
80
|
+
const isDataPrivate = descriptor.privacy
|
|
81
|
+
? !descriptor.privacy.dataLeavesDevice
|
|
82
|
+
: descriptor.type !== "cloud";
|
|
74
83
|
if (constraints.cloud === "forbidden" && descriptor.type === "cloud") {
|
|
75
84
|
return false;
|
|
76
85
|
}
|
|
77
86
|
if (constraints.cloud === "required" && descriptor.type !== "cloud") {
|
|
78
87
|
return false;
|
|
79
88
|
}
|
|
80
|
-
if (constraints.privacy === "local_required" && !
|
|
89
|
+
if (constraints.privacy === "local_required" && !isDataPrivate) {
|
|
81
90
|
return false;
|
|
82
91
|
}
|
|
83
92
|
if (constraints.privacy === "cloud_required" && descriptor.type !== "cloud") {
|
|
@@ -88,10 +97,7 @@ export class Router {
|
|
|
88
97
|
}
|
|
89
98
|
return candidate.capabilities.available;
|
|
90
99
|
});
|
|
91
|
-
|
|
92
|
-
? [selected, ...eligible.filter((candidate) => candidate !== selected)]
|
|
93
|
-
: [];
|
|
94
|
-
if (ordered.length === 0) {
|
|
100
|
+
if (admissible.length === 0) {
|
|
95
101
|
const failureSummary = this.buildFallbackFailureSummary({
|
|
96
102
|
online,
|
|
97
103
|
constraints: planInput.constraints,
|
|
@@ -114,17 +120,17 @@ export class Router {
|
|
|
114
120
|
}
|
|
115
121
|
};
|
|
116
122
|
}
|
|
117
|
-
const selectedProviderId =
|
|
123
|
+
const selectedProviderId = admissible[0]?.descriptor.id;
|
|
118
124
|
return {
|
|
119
125
|
selectedProviderId,
|
|
120
|
-
fallbackProviderIds:
|
|
121
|
-
candidates:
|
|
126
|
+
fallbackProviderIds: admissible.slice(1).map((candidate) => candidate.descriptor.id),
|
|
127
|
+
candidates: admissible.map((candidate, index) => ({
|
|
122
128
|
providerId: candidate.descriptor.id,
|
|
123
129
|
order: index
|
|
124
130
|
})),
|
|
125
131
|
rejectedProviders: [],
|
|
126
132
|
explanation: {
|
|
127
|
-
summary: `Selected provider '${selectedProviderId}' deterministically from ${
|
|
133
|
+
summary: `Selected provider '${selectedProviderId}' deterministically from ${admissible.length} eligible candidate(s).`,
|
|
128
134
|
selectedProviderId
|
|
129
135
|
}
|
|
130
136
|
};
|