@independo/inderun-web 0.3.3 → 0.4.0-rc.2
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 +46 -9
- package/dist/core/engine.d.ts +26 -2
- package/dist/core/engine.d.ts.map +1 -1
- package/dist/core/engine.js +78 -34
- package/dist/core/errors.d.ts +13 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +32 -0
- package/dist/core/model-profiles.d.ts +34 -0
- package/dist/core/model-profiles.d.ts.map +1 -0
- package/dist/core/model-profiles.js +49 -0
- package/dist/core/provenance.d.ts +32 -0
- package/dist/core/provenance.d.ts.map +1 -0
- package/dist/core/provenance.js +71 -0
- package/dist/core/provider.d.ts +13 -0
- package/dist/core/provider.d.ts.map +1 -1
- package/dist/core/route-planner.d.ts +1 -1
- package/dist/core/route-planner.d.ts.map +1 -1
- package/dist/core/route-planner.js +2 -1
- package/dist/core/router.d.ts +4 -3
- package/dist/core/router.d.ts.map +1 -1
- package/dist/core/router.js +4 -3
- package/dist/facade/text.d.ts +123 -0
- package/dist/facade/text.d.ts.map +1 -0
- package/dist/facade/text.js +234 -0
- package/dist/index.d.ts +36 -45
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -38
- package/dist/provider.d.ts +34 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +33 -0
- package/dist/providers/onnx/provider.d.ts.map +1 -1
- package/dist/providers/onnx/provider.js +1 -0
- package/dist/providers/openai/provider.d.ts +41 -11
- package/dist/providers/openai/provider.d.ts.map +1 -1
- package/dist/providers/openai/provider.js +168 -58
- package/dist/web-sdk.d.ts +21 -3
- package/dist/web-sdk.d.ts.map +1 -1
- package/dist/web-sdk.js +23 -6
- package/package.json +7 -3
- package/src/core/engine.ts +114 -35
- package/src/core/errors.ts +35 -1
- package/src/core/model-profiles.ts +70 -0
- package/src/core/provenance.ts +90 -0
- package/src/core/provider.ts +13 -0
- package/src/core/route-planner.ts +3 -1
- package/src/core/router.ts +12 -3
- package/src/facade/text.ts +349 -0
- package/src/index.ts +49 -95
- package/src/provider.ts +77 -0
- package/src/providers/onnx/provider.ts +1 -0
- package/src/providers/openai/provider.ts +232 -68
- package/src/web-sdk.ts +47 -10
package/src/core/engine.ts
CHANGED
|
@@ -19,6 +19,14 @@ import {
|
|
|
19
19
|
} from "./errors.js";
|
|
20
20
|
import { type TelemetryEvent, type TelemetryService, NoOpTelemetryService } from "./telemetry.js";
|
|
21
21
|
import { EventGate } from "./event-gate.js";
|
|
22
|
+
import {
|
|
23
|
+
AUTO_MODEL_PROFILE,
|
|
24
|
+
type ModelProfiles,
|
|
25
|
+
type ResolvedModelProfile,
|
|
26
|
+
resolveModelProfile,
|
|
27
|
+
validateModelProfiles
|
|
28
|
+
} from "./model-profiles.js";
|
|
29
|
+
import { ProvenanceRecorder } from "./provenance.js";
|
|
22
30
|
|
|
23
31
|
/**
|
|
24
32
|
* @deprecated Renamed to `StreamRun`, which is the name the generated
|
|
@@ -27,6 +35,22 @@ import { EventGate } from "./event-gate.js";
|
|
|
27
35
|
*/
|
|
28
36
|
export type StreamHandleResult = StreamRun;
|
|
29
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Optional engine configuration.
|
|
40
|
+
*/
|
|
41
|
+
export interface IndeRunOptions {
|
|
42
|
+
/**
|
|
43
|
+
* Telemetry listener. Defaults to the host's telemetry service, then to a no-op.
|
|
44
|
+
*/
|
|
45
|
+
telemetryService?: TelemetryService;
|
|
46
|
+
/**
|
|
47
|
+
* Named model profiles requests can route within (`TaskRequest.modelProfile`).
|
|
48
|
+
* Each maps a profile name to an ordered list of target (provider instance) ids.
|
|
49
|
+
* `"auto"` is built in and cannot be redefined. See `docs/architecture/api-layering.md`.
|
|
50
|
+
*/
|
|
51
|
+
modelProfiles?: ModelProfiles;
|
|
52
|
+
}
|
|
53
|
+
|
|
30
54
|
/**
|
|
31
55
|
* Main orchestrator SDK entrypoint class.
|
|
32
56
|
* Validates tasks, executes routing rules based on request constraints and connectivity,
|
|
@@ -35,21 +59,48 @@ export type StreamHandleResult = StreamRun;
|
|
|
35
59
|
export class IndeRun implements IndeRunApi {
|
|
36
60
|
private router: Router;
|
|
37
61
|
private telemetryService: TelemetryService;
|
|
62
|
+
private modelProfiles: ModelProfiles;
|
|
38
63
|
|
|
39
64
|
/**
|
|
40
65
|
* Initializes the IndeRun instance.
|
|
41
66
|
* @param registry - Registry filled with active providers.
|
|
42
67
|
* @param hostServices - Services wrapping OS interfaces and indicators.
|
|
43
|
-
* @param
|
|
68
|
+
* @param options - Optional telemetry listener and model profiles.
|
|
69
|
+
* @throws {Error} If a model profile is malformed (see {@link validateModelProfiles}).
|
|
44
70
|
*/
|
|
45
71
|
constructor(
|
|
46
72
|
private registry: ProviderRegistry,
|
|
47
73
|
private hostServices: HostServices,
|
|
48
|
-
|
|
74
|
+
options: IndeRunOptions = {}
|
|
49
75
|
) {
|
|
50
76
|
this.router = new Router(this.registry);
|
|
51
77
|
this.telemetryService =
|
|
52
|
-
telemetryService || this.hostServices.telemetry || new NoOpTelemetryService();
|
|
78
|
+
options.telemetryService || this.hostServices.telemetry || new NoOpTelemetryService();
|
|
79
|
+
this.modelProfiles = options.modelProfiles ?? {};
|
|
80
|
+
validateModelProfiles(this.modelProfiles);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validates the request against the schema and resolves its model profile.
|
|
85
|
+
* Both failures are request validation errors: they are reported before any
|
|
86
|
+
* provider is probed, with the offending fields in `details.validationIssues`.
|
|
87
|
+
*/
|
|
88
|
+
private validateRequest(request: TaskRequest, runId: string): ResolvedModelProfile {
|
|
89
|
+
const validationIssues = getTaskRequestValidationIssues(request);
|
|
90
|
+
if (validationIssues.length === 0) {
|
|
91
|
+
const resolved = resolveModelProfile(this.modelProfiles, request.modelProfile);
|
|
92
|
+
if (resolved.known) {
|
|
93
|
+
return resolved.profile;
|
|
94
|
+
}
|
|
95
|
+
validationIssues.push({
|
|
96
|
+
path: "/modelProfile",
|
|
97
|
+
message: `unknown model profile "${request.modelProfile}"; configure it in the engine's modelProfiles`
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const message = `Validation failed for TaskRequest: ${validationIssues
|
|
101
|
+
.map((i) => `${i.path} - ${i.message}`)
|
|
102
|
+
.join("; ")}`;
|
|
103
|
+
throw createInternal(message, { runId, details: { validationIssues } });
|
|
53
104
|
}
|
|
54
105
|
|
|
55
106
|
/**
|
|
@@ -81,6 +132,7 @@ export class IndeRun implements IndeRunApi {
|
|
|
81
132
|
rejectedProviders: routeSelection.routePlan.rejectedProviders,
|
|
82
133
|
fallbackAvailable: providerCount > 1,
|
|
83
134
|
taskKind: request.task.kind,
|
|
135
|
+
modelProfile: request.modelProfile ?? "auto",
|
|
84
136
|
constraints: request.constraints ?? null,
|
|
85
137
|
preferences: request.preferences ?? null,
|
|
86
138
|
explanation: routeSelection.explanation
|
|
@@ -137,23 +189,21 @@ export class IndeRun implements IndeRunApi {
|
|
|
137
189
|
const startTime = this.now();
|
|
138
190
|
|
|
139
191
|
const runId = request.requestId || `run_${Math.random().toString(36).substring(2, 11)}`;
|
|
192
|
+
const provenance = new ProvenanceRecorder(request.modelProfile ?? AUTO_MODEL_PROFILE);
|
|
140
193
|
|
|
141
194
|
try {
|
|
142
|
-
// 1. Validate request payload using schema contracts
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// 2. Select the route based on constraints and host capabilities
|
|
155
|
-
const routeSelection = await this.router.selectRoute(request, this.hostServices, "run");
|
|
195
|
+
// 1. Validate request payload using schema contracts and resolve its model profile
|
|
196
|
+
const modelProfile = this.validateRequest(request, runId);
|
|
197
|
+
|
|
198
|
+
// 2. Select the route based on constraints, model profile, and host capabilities
|
|
199
|
+
const routeSelection = await this.router.selectRoute(
|
|
200
|
+
request,
|
|
201
|
+
this.hostServices,
|
|
202
|
+
"run",
|
|
203
|
+
modelProfile
|
|
204
|
+
);
|
|
156
205
|
const providers = [routeSelection.provider, ...routeSelection.fallbackProviders];
|
|
206
|
+
provenance.routed(routeSelection.routePlan);
|
|
157
207
|
|
|
158
208
|
this.emitRouteDecided(runId, request, routeSelection, providers.length);
|
|
159
209
|
|
|
@@ -179,6 +229,8 @@ export class IndeRun implements IndeRunApi {
|
|
|
179
229
|
providerUsed: providerId,
|
|
180
230
|
totalMs
|
|
181
231
|
};
|
|
232
|
+
provenance.attempted(providerId, "succeeded");
|
|
233
|
+
result.provenance = provenance.build(provider.describe(), result.telemetry.reportedModel);
|
|
182
234
|
|
|
183
235
|
this.safeEmit({
|
|
184
236
|
type: "attempt_succeeded",
|
|
@@ -194,7 +246,7 @@ export class IndeRun implements IndeRunApi {
|
|
|
194
246
|
|
|
195
247
|
return result;
|
|
196
248
|
} catch (err) {
|
|
197
|
-
|
|
249
|
+
const attemptError = toIndeRunException(err, {
|
|
198
250
|
providerId,
|
|
199
251
|
runId,
|
|
200
252
|
details: {
|
|
@@ -203,6 +255,8 @@ export class IndeRun implements IndeRunApi {
|
|
|
203
255
|
routePlan: routeSelection.routePlan
|
|
204
256
|
}
|
|
205
257
|
});
|
|
258
|
+
provenance.attempted(providerId, "failed", attemptError.errorClass);
|
|
259
|
+
lastError = attemptError;
|
|
206
260
|
}
|
|
207
261
|
}
|
|
208
262
|
|
|
@@ -213,14 +267,14 @@ export class IndeRun implements IndeRunApi {
|
|
|
213
267
|
details: {
|
|
214
268
|
totalMs,
|
|
215
269
|
attemptedProviderIds,
|
|
216
|
-
fallbackOccurred:
|
|
270
|
+
fallbackOccurred: attemptedProviderIds.length > 1,
|
|
217
271
|
routePlan: routeSelection.routePlan
|
|
218
272
|
}
|
|
219
273
|
});
|
|
220
274
|
|
|
221
275
|
throw exception;
|
|
222
276
|
} catch (error) {
|
|
223
|
-
const
|
|
277
|
+
const normalized = toIndeRunException(error, {
|
|
224
278
|
runId,
|
|
225
279
|
details: {
|
|
226
280
|
totalMs:
|
|
@@ -229,6 +283,10 @@ export class IndeRun implements IndeRunApi {
|
|
|
229
283
|
: this.now() - startTime
|
|
230
284
|
}
|
|
231
285
|
});
|
|
286
|
+
// A routing refusal throws before any attempt; its rejections travel on the details.
|
|
287
|
+
provenance.routingFailed(normalized.details);
|
|
288
|
+
// No attempt produced the result, so the outcome is attributed to no provider.
|
|
289
|
+
const exception = normalized.withProvenance(provenance.build());
|
|
232
290
|
const endTime = this.now();
|
|
233
291
|
const totalMs =
|
|
234
292
|
typeof (exception as { details?: { totalMs?: number } }).details?.totalMs === "number"
|
|
@@ -273,13 +331,13 @@ export class IndeRun implements IndeRunApi {
|
|
|
273
331
|
const startTime = this.now();
|
|
274
332
|
const now = () => this.now();
|
|
275
333
|
const runId = request.requestId || `run_${Math.random().toString(36).substring(2, 11)}`;
|
|
334
|
+
const provenance = new ProvenanceRecorder(request.modelProfile ?? AUTO_MODEL_PROFILE);
|
|
276
335
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
throw createInternal(message, { runId, details: { validationIssues } });
|
|
336
|
+
let modelProfile: ResolvedModelProfile;
|
|
337
|
+
try {
|
|
338
|
+
modelProfile = this.validateRequest(request, runId);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
throw toIndeRunException(error, { runId }).withProvenance(provenance.build());
|
|
283
341
|
}
|
|
284
342
|
|
|
285
343
|
// Route selection failures are the one class of stream error that surfaces as a
|
|
@@ -287,11 +345,19 @@ export class IndeRun implements IndeRunApi {
|
|
|
287
345
|
// runId — there is no handle for the caller to correlate them with otherwise.
|
|
288
346
|
let routeSelection: RouteSelection;
|
|
289
347
|
try {
|
|
290
|
-
routeSelection = await this.router.selectRoute(
|
|
348
|
+
routeSelection = await this.router.selectRoute(
|
|
349
|
+
request,
|
|
350
|
+
this.hostServices,
|
|
351
|
+
"stream",
|
|
352
|
+
modelProfile
|
|
353
|
+
);
|
|
291
354
|
} catch (error) {
|
|
292
|
-
|
|
355
|
+
const exception = toIndeRunException(error, { runId });
|
|
356
|
+
provenance.routingFailed(exception.details);
|
|
357
|
+
throw exception.withProvenance(provenance.build());
|
|
293
358
|
}
|
|
294
359
|
const providers = [routeSelection.provider, ...routeSelection.fallbackProviders];
|
|
360
|
+
provenance.routed(routeSelection.routePlan);
|
|
295
361
|
|
|
296
362
|
this.emitRouteDecided(runId, request, routeSelection, providers.length);
|
|
297
363
|
|
|
@@ -322,15 +388,22 @@ export class IndeRun implements IndeRunApi {
|
|
|
322
388
|
const attemptedProviderIds: string[] = [];
|
|
323
389
|
let committed = false;
|
|
324
390
|
|
|
325
|
-
const cancelledOutcome = (
|
|
391
|
+
const cancelledOutcome = (
|
|
392
|
+
partialText: string,
|
|
393
|
+
cancelledProvider?: ProviderAdapter
|
|
394
|
+
): StreamTerminalOutcome => {
|
|
326
395
|
const reason =
|
|
327
396
|
typeof controller.signal.reason === "string" ? controller.signal.reason : undefined;
|
|
397
|
+
if (cancelledProvider !== undefined) {
|
|
398
|
+
provenance.attempted(cancelledProvider.describe().id, "cancelled");
|
|
399
|
+
}
|
|
328
400
|
return {
|
|
329
401
|
outcome: "cancelled",
|
|
330
402
|
runId,
|
|
331
403
|
schemaVersion: "1.0",
|
|
332
404
|
partialText,
|
|
333
|
-
...(reason !== undefined ? { reason } : {})
|
|
405
|
+
...(reason !== undefined ? { reason } : {}),
|
|
406
|
+
provenance: provenance.build(cancelledProvider?.describe())
|
|
334
407
|
};
|
|
335
408
|
};
|
|
336
409
|
|
|
@@ -387,6 +460,7 @@ export class IndeRun implements IndeRunApi {
|
|
|
387
460
|
}
|
|
388
461
|
|
|
389
462
|
// ev.kind === "done"
|
|
463
|
+
provenance.attempted(providerId, "succeeded");
|
|
390
464
|
const outcome: StreamTerminalOutcome = {
|
|
391
465
|
outcome: "completed",
|
|
392
466
|
runId,
|
|
@@ -394,7 +468,8 @@ export class IndeRun implements IndeRunApi {
|
|
|
394
468
|
finalText: ev.finalText,
|
|
395
469
|
telemetry: { providerUsed: providerId, totalMs: now() - startTime },
|
|
396
470
|
...(ev.finishReason !== undefined ? { finishReason: ev.finishReason } : {}),
|
|
397
|
-
...(ev.usage !== undefined ? { usage: ev.usage } : {})
|
|
471
|
+
...(ev.usage !== undefined ? { usage: ev.usage } : {}),
|
|
472
|
+
provenance: provenance.build(provider.describe(), ev.reportedModel)
|
|
398
473
|
};
|
|
399
474
|
const terminal = gate.terminate(outcome, now());
|
|
400
475
|
safeEmit({
|
|
@@ -413,7 +488,7 @@ export class IndeRun implements IndeRunApi {
|
|
|
413
488
|
}
|
|
414
489
|
|
|
415
490
|
if (controller.signal.aborted) {
|
|
416
|
-
const terminal = gate.terminate(cancelledOutcome(partialText), now());
|
|
491
|
+
const terminal = gate.terminate(cancelledOutcome(partialText, provider), now());
|
|
417
492
|
safeEmit({
|
|
418
493
|
type: "stream_cancelled",
|
|
419
494
|
runId,
|
|
@@ -428,7 +503,7 @@ export class IndeRun implements IndeRunApi {
|
|
|
428
503
|
throw new Error(`Provider '${providerId}' stream ended without a terminal event.`);
|
|
429
504
|
} catch (err) {
|
|
430
505
|
if (controller.signal.aborted) {
|
|
431
|
-
const terminal = gate.terminate(cancelledOutcome(partialText), now());
|
|
506
|
+
const terminal = gate.terminate(cancelledOutcome(partialText, provider), now());
|
|
432
507
|
safeEmit({
|
|
433
508
|
type: "stream_cancelled",
|
|
434
509
|
runId,
|
|
@@ -444,6 +519,7 @@ export class IndeRun implements IndeRunApi {
|
|
|
444
519
|
runId,
|
|
445
520
|
details: { attemptedProviderIds }
|
|
446
521
|
});
|
|
522
|
+
provenance.attempted(providerId, "failed", exception.errorClass);
|
|
447
523
|
|
|
448
524
|
if (!committed) {
|
|
449
525
|
safeEmit({
|
|
@@ -459,12 +535,14 @@ export class IndeRun implements IndeRunApi {
|
|
|
459
535
|
continue;
|
|
460
536
|
}
|
|
461
537
|
|
|
538
|
+
// Committed: the failure belongs to this provider, not to the chain.
|
|
462
539
|
const outcome: StreamTerminalOutcome = {
|
|
463
540
|
outcome: "error",
|
|
464
541
|
runId,
|
|
465
542
|
schemaVersion: "1.0",
|
|
466
543
|
error: exception.toContractError(),
|
|
467
|
-
partialText
|
|
544
|
+
partialText,
|
|
545
|
+
provenance: provenance.build(provider.describe())
|
|
468
546
|
};
|
|
469
547
|
const terminal = gate.terminate(outcome, now());
|
|
470
548
|
safeEmit({
|
|
@@ -505,7 +583,8 @@ export class IndeRun implements IndeRunApi {
|
|
|
505
583
|
runId,
|
|
506
584
|
schemaVersion: "1.0",
|
|
507
585
|
error: exception.toContractError(),
|
|
508
|
-
partialText: ""
|
|
586
|
+
partialText: "",
|
|
587
|
+
provenance: provenance.build()
|
|
509
588
|
};
|
|
510
589
|
const terminal = gate.terminate(outcome, now());
|
|
511
590
|
safeEmit({
|
package/src/core/errors.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ExecutionProvenance,
|
|
3
|
+
IndeRunError,
|
|
4
|
+
IndeRunErrorClass
|
|
5
|
+
} from "@independo/inderun-contracts";
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
8
|
* Parameter structure required to initialize an IndeRunException.
|
|
@@ -32,6 +36,10 @@ export interface IndeRunExceptionParams {
|
|
|
32
36
|
* Rich contextual metadata regarding the error.
|
|
33
37
|
*/
|
|
34
38
|
details?: Record<string, unknown>;
|
|
39
|
+
/**
|
|
40
|
+
* How the failed execution played out, attached by the engine.
|
|
41
|
+
*/
|
|
42
|
+
provenance?: ExecutionProvenance;
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
/**
|
|
@@ -53,6 +61,8 @@ export class IndeRunException extends Error {
|
|
|
53
61
|
readonly retryAfterMs?: number;
|
|
54
62
|
/** Custom details and metadata context. */
|
|
55
63
|
readonly details?: Record<string, unknown>;
|
|
64
|
+
/** How the failed execution played out: attempts, rejections, the provider committed to. */
|
|
65
|
+
readonly provenance?: ExecutionProvenance;
|
|
56
66
|
|
|
57
67
|
constructor(params: IndeRunExceptionParams) {
|
|
58
68
|
super(params.message);
|
|
@@ -63,6 +73,7 @@ export class IndeRunException extends Error {
|
|
|
63
73
|
if (params.retryable !== undefined) this.retryable = params.retryable;
|
|
64
74
|
if (params.retryAfterMs !== undefined) this.retryAfterMs = params.retryAfterMs;
|
|
65
75
|
if (params.details !== undefined) this.details = params.details;
|
|
76
|
+
if (params.provenance !== undefined) this.provenance = params.provenance;
|
|
66
77
|
|
|
67
78
|
Object.setPrototypeOf(this, IndeRunException.prototype);
|
|
68
79
|
}
|
|
@@ -81,8 +92,28 @@ export class IndeRunException extends Error {
|
|
|
81
92
|
if (this.retryable !== undefined) err.retryable = this.retryable;
|
|
82
93
|
if (this.retryAfterMs !== undefined) err.retryAfterMs = this.retryAfterMs;
|
|
83
94
|
if (this.details !== undefined) err.details = this.details;
|
|
95
|
+
if (this.provenance !== undefined) err.provenance = this.provenance;
|
|
84
96
|
return err;
|
|
85
97
|
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A copy of this exception carrying `provenance`. The engine attaches provenance once it
|
|
101
|
+
* knows how the whole execution ended, replacing anything set earlier.
|
|
102
|
+
*/
|
|
103
|
+
withProvenance(provenance: ExecutionProvenance): IndeRunException {
|
|
104
|
+
return new IndeRunException({ ...this.params(), provenance });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private params(): IndeRunExceptionParams {
|
|
108
|
+
const params: IndeRunExceptionParams = { errorClass: this.errorClass, message: this.message };
|
|
109
|
+
if (this.runId !== undefined) params.runId = this.runId;
|
|
110
|
+
if (this.providerId !== undefined) params.providerId = this.providerId;
|
|
111
|
+
if (this.retryable !== undefined) params.retryable = this.retryable;
|
|
112
|
+
if (this.retryAfterMs !== undefined) params.retryAfterMs = this.retryAfterMs;
|
|
113
|
+
if (this.details !== undefined) params.details = this.details;
|
|
114
|
+
if (this.provenance !== undefined) params.provenance = this.provenance;
|
|
115
|
+
return params;
|
|
116
|
+
}
|
|
86
117
|
}
|
|
87
118
|
|
|
88
119
|
/**
|
|
@@ -189,6 +220,9 @@ export function toIndeRunException(
|
|
|
189
220
|
};
|
|
190
221
|
}
|
|
191
222
|
|
|
223
|
+
const provenance = error.provenance ?? fallbackParams.provenance;
|
|
224
|
+
if (provenance !== undefined) mergedParams.provenance = provenance;
|
|
225
|
+
|
|
192
226
|
return new IndeRunException(mergedParams);
|
|
193
227
|
}
|
|
194
228
|
return error;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { SharedPlannerInput } from "./route-planner.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The built-in profile: every registered target, ranked by constraints and
|
|
5
|
+
* preferences alone. Requests that name no profile route within it.
|
|
6
|
+
*/
|
|
7
|
+
export const AUTO_MODEL_PROFILE = "auto";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Application-defined model profiles: a profile name mapped to an ordered list of
|
|
11
|
+
* target ids (provider instance ids, each bound to one model). Requests name a
|
|
12
|
+
* profile through `TaskRequest.modelProfile`; the route planner treats the list as
|
|
13
|
+
* an allow-list and ranks its entries after placement and before preferences.
|
|
14
|
+
* Ids that name no registered provider are ignored by the planner.
|
|
15
|
+
*/
|
|
16
|
+
export type ModelProfiles = Readonly<Record<string, readonly string[]>>;
|
|
17
|
+
|
|
18
|
+
/** A resolved profile as the shared planner consumes it; absent means `auto`. */
|
|
19
|
+
export type ResolvedModelProfile = SharedPlannerInput["modelProfile"];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Checks profile configuration once, when the engine is constructed, so a
|
|
23
|
+
* malformed profile is a setup error rather than a per-request surprise.
|
|
24
|
+
* @throws {Error} When a profile is named `auto`, is empty, or lists a target twice.
|
|
25
|
+
*/
|
|
26
|
+
export function validateModelProfiles(profiles: ModelProfiles): void {
|
|
27
|
+
for (const [name, targetIds] of Object.entries(profiles)) {
|
|
28
|
+
if (name.length === 0) {
|
|
29
|
+
throw new Error("Model profile names must be non-empty.");
|
|
30
|
+
}
|
|
31
|
+
if (name === AUTO_MODEL_PROFILE) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`"${AUTO_MODEL_PROFILE}" is the built-in model profile and cannot be redefined.`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (targetIds.length === 0) {
|
|
37
|
+
throw new Error(`Model profile "${name}" must list at least one target id.`);
|
|
38
|
+
}
|
|
39
|
+
const seen = new Set<string>();
|
|
40
|
+
for (const id of targetIds) {
|
|
41
|
+
if (id.length === 0) {
|
|
42
|
+
throw new Error(`Model profile "${name}" lists an empty target id.`);
|
|
43
|
+
}
|
|
44
|
+
if (seen.has(id)) {
|
|
45
|
+
throw new Error(`Model profile "${name}" lists target "${id}" more than once.`);
|
|
46
|
+
}
|
|
47
|
+
seen.add(id);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolves a request's profile name against the configured profiles.
|
|
54
|
+
* @returns `{ known: true }` with the planner input (undefined for `auto`), or
|
|
55
|
+
* `{ known: false }` when the name matches no configured profile.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveModelProfile(
|
|
58
|
+
profiles: ModelProfiles,
|
|
59
|
+
name: string | undefined
|
|
60
|
+
): { known: true; profile: ResolvedModelProfile } | { known: false } {
|
|
61
|
+
if (name === undefined || name === AUTO_MODEL_PROFILE) {
|
|
62
|
+
return { known: true, profile: undefined };
|
|
63
|
+
}
|
|
64
|
+
if (!Object.hasOwn(profiles, name)) {
|
|
65
|
+
return { known: false };
|
|
66
|
+
}
|
|
67
|
+
// validateModelProfiles guarantees every configured profile is non-empty.
|
|
68
|
+
const [first, ...rest] = profiles[name]!;
|
|
69
|
+
return { known: true, profile: { name, targetIds: [first!, ...rest] } };
|
|
70
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ExecutionProvenance, IndeRunErrorClass } from "@independo/inderun-contracts";
|
|
2
|
+
import type { ProviderDescriptor } from "./provider.js";
|
|
3
|
+
import type { SharedPlannerRoutePlan } from "./route-planner.js";
|
|
4
|
+
import { AUTO_MODEL_PROFILE } from "./model-profiles.js";
|
|
5
|
+
|
|
6
|
+
type ExecutionProvenanceAttempt = ExecutionProvenance["attempts"][number];
|
|
7
|
+
type RouteRejection = ExecutionProvenance["routeRejections"][number];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Accumulates what one execution did -- each attempt and how it ended -- and renders it as the
|
|
11
|
+
* normalized {@link ExecutionProvenance} carried on results, terminal outcomes, and errors.
|
|
12
|
+
*
|
|
13
|
+
* Everything recorded is either observed by the engine (attempts, their outcomes) or declared or
|
|
14
|
+
* reported by a provider (descriptor type and model, the model a response names). Nothing is
|
|
15
|
+
* inferred: an unknown is left absent or reported as `unknown`.
|
|
16
|
+
*/
|
|
17
|
+
export class ProvenanceRecorder {
|
|
18
|
+
private readonly attempts: ExecutionProvenanceAttempt[] = [];
|
|
19
|
+
private routeRejections: RouteRejection[] = [];
|
|
20
|
+
|
|
21
|
+
constructor(private readonly modelProfile: string = AUTO_MODEL_PROFILE) {}
|
|
22
|
+
|
|
23
|
+
/** Records the planner's rejections; codes only, never the provider-supplied messages. */
|
|
24
|
+
routed(routePlan: SharedPlannerRoutePlan): void {
|
|
25
|
+
this.routeRejections = routeRejectionsFrom(routePlan.rejectedProviders);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Records the rejections a routing failure carried out on its exception's details. */
|
|
29
|
+
routingFailed(details: Record<string, unknown> | undefined): void {
|
|
30
|
+
const rejected = details?.rejectedProviders;
|
|
31
|
+
if (Array.isArray(rejected)) {
|
|
32
|
+
this.routeRejections = routeRejectionsFrom(
|
|
33
|
+
rejected as SharedPlannerRoutePlan["rejectedProviders"]
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Records how one provider attempt ended. */
|
|
39
|
+
attempted(
|
|
40
|
+
providerId: string,
|
|
41
|
+
outcome: ExecutionProvenanceAttempt["outcome"],
|
|
42
|
+
errorClass?: IndeRunErrorClass
|
|
43
|
+
): void {
|
|
44
|
+
this.attempts.push({
|
|
45
|
+
providerId,
|
|
46
|
+
outcome,
|
|
47
|
+
...(errorClass !== undefined ? { errorClass } : {})
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Renders the record. `attributedTo` is the provider the outcome belongs to -- the one that
|
|
53
|
+
* produced the result, the one a stream was committed to, or the one running at cancellation --
|
|
54
|
+
* and is absent when no provider was reached or every attempt failed before a commit.
|
|
55
|
+
*/
|
|
56
|
+
build(attributedTo?: ProviderDescriptor, reportedModel?: string): ExecutionProvenance {
|
|
57
|
+
return {
|
|
58
|
+
schemaVersion: "1.0",
|
|
59
|
+
modelProfile: this.modelProfile,
|
|
60
|
+
...(attributedTo !== undefined ? { providerId: attributedTo.id } : {}),
|
|
61
|
+
locality: attributedTo !== undefined ? localityOf(attributedTo) : "unknown",
|
|
62
|
+
...(attributedTo?.model !== undefined ? { configuredModel: attributedTo.model } : {}),
|
|
63
|
+
...(attributedTo !== undefined && reportedModel !== undefined ? { reportedModel } : {}),
|
|
64
|
+
attemptCount: this.attempts.length,
|
|
65
|
+
fallbackOccurred: this.attempts.length > 1,
|
|
66
|
+
attempts: [...this.attempts],
|
|
67
|
+
routeRejections: [...this.routeRejections]
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function localityOf(descriptor: ProviderDescriptor): ExecutionProvenance["locality"] {
|
|
73
|
+
switch (descriptor.type) {
|
|
74
|
+
case "local":
|
|
75
|
+
return "on_device";
|
|
76
|
+
case "edge":
|
|
77
|
+
return "edge";
|
|
78
|
+
case "cloud":
|
|
79
|
+
return "cloud";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function routeRejectionsFrom(
|
|
84
|
+
rejected: SharedPlannerRoutePlan["rejectedProviders"]
|
|
85
|
+
): RouteRejection[] {
|
|
86
|
+
return rejected.map((provider) => ({
|
|
87
|
+
providerId: provider.providerId,
|
|
88
|
+
codes: provider.reasons.map((reason) => reason.code)
|
|
89
|
+
}));
|
|
90
|
+
}
|
package/src/core/provider.ts
CHANGED
|
@@ -19,6 +19,14 @@ export interface ProviderDescriptor {
|
|
|
19
19
|
* Execution target type: 'local' (on-device), 'edge', or 'cloud'.
|
|
20
20
|
*/
|
|
21
21
|
type: "local" | "edge" | "cloud";
|
|
22
|
+
/**
|
|
23
|
+
* The model this provider instance is bound to, when it can name one. A provider
|
|
24
|
+
* instance bound to one model is a *target* that model profiles list by `id`.
|
|
25
|
+
* Cloud providers report the model id they send; platform-managed models report
|
|
26
|
+
* an opaque platform alias rather than a guessed weight name. Absent means the
|
|
27
|
+
* provider cannot name its model.
|
|
28
|
+
*/
|
|
29
|
+
model?: string;
|
|
22
30
|
/**
|
|
23
31
|
* Connection transport layer protocol.
|
|
24
32
|
*/
|
|
@@ -188,6 +196,11 @@ export type ProviderStreamEvent =
|
|
|
188
196
|
*/
|
|
189
197
|
finishReason?: TaskResult["finishReason"];
|
|
190
198
|
usage?: TaskResult["usage"];
|
|
199
|
+
/**
|
|
200
|
+
* The model identifier the provider's backend reported for this run, when it reports
|
|
201
|
+
* one. Carried into the terminal outcome's provenance; never inferred from configuration.
|
|
202
|
+
*/
|
|
203
|
+
reportedModel?: string;
|
|
191
204
|
}
|
|
192
205
|
| { kind: "error"; error: unknown };
|
|
193
206
|
|
|
@@ -177,7 +177,8 @@ export function buildSharedPlannerInput(
|
|
|
177
177
|
request: TaskRequest,
|
|
178
178
|
snapshots: ProviderRuntimeSnapshot[],
|
|
179
179
|
networkOnline: boolean,
|
|
180
|
-
interactionMode: InteractionMode = "run"
|
|
180
|
+
interactionMode: InteractionMode = "run",
|
|
181
|
+
modelProfile?: SharedPlannerInput["modelProfile"]
|
|
181
182
|
): SharedPlannerInput {
|
|
182
183
|
const constraints = request.constraints ?? {};
|
|
183
184
|
const preferences = request.preferences ?? {};
|
|
@@ -196,6 +197,7 @@ export function buildSharedPlannerInput(
|
|
|
196
197
|
preferences: {
|
|
197
198
|
optimizeFor: preferences.optimizeFor ?? "balanced"
|
|
198
199
|
},
|
|
200
|
+
...(modelProfile ? { modelProfile } : {}),
|
|
199
201
|
providers: snapshots.map((snapshot) => ({
|
|
200
202
|
descriptor: snapshot.descriptor,
|
|
201
203
|
capabilities: snapshot.capabilities
|
package/src/core/router.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type InteractionMode,
|
|
15
15
|
type ProviderRuntimeSnapshot,
|
|
16
16
|
type RoutePlanner,
|
|
17
|
+
type SharedPlannerInput,
|
|
17
18
|
type SharedPlannerRoutePlan,
|
|
18
19
|
WasmRoutePlanner
|
|
19
20
|
} from "./route-planner.js";
|
|
@@ -63,17 +64,25 @@ export class Router {
|
|
|
63
64
|
*
|
|
64
65
|
* The requested `interactionMode` is a planning input, not a post-filter: the
|
|
65
66
|
* shared planner rejects providers that cannot satisfy it and says why, so the
|
|
66
|
-
* whole returned chain is mode-compatible.
|
|
67
|
+
* whole returned chain is mode-compatible. The resolved model profile is a
|
|
68
|
+
* planning input in the same way: the planner rejects targets outside it.
|
|
67
69
|
*/
|
|
68
70
|
async selectRoute(
|
|
69
71
|
request: TaskRequest,
|
|
70
72
|
hostServices: HostServices,
|
|
71
|
-
interactionMode: InteractionMode = "run"
|
|
73
|
+
interactionMode: InteractionMode = "run",
|
|
74
|
+
modelProfile?: SharedPlannerInput["modelProfile"]
|
|
72
75
|
): Promise<RouteSelection> {
|
|
73
76
|
const online = await hostServices.connectivity.isOnline();
|
|
74
77
|
const snapshots = await collectProviderRuntimeSnapshots(this.registry.list(), hostServices);
|
|
75
78
|
|
|
76
|
-
const planInput = buildSharedPlannerInput(
|
|
79
|
+
const planInput = buildSharedPlannerInput(
|
|
80
|
+
request,
|
|
81
|
+
snapshots,
|
|
82
|
+
online,
|
|
83
|
+
interactionMode,
|
|
84
|
+
modelProfile
|
|
85
|
+
);
|
|
77
86
|
const outcome = await this.planner.planRoute(planInput);
|
|
78
87
|
|
|
79
88
|
if (!outcome.routePlan) {
|