@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.
Files changed (52) hide show
  1. package/README.md +46 -9
  2. package/dist/core/engine.d.ts +26 -2
  3. package/dist/core/engine.d.ts.map +1 -1
  4. package/dist/core/engine.js +78 -34
  5. package/dist/core/errors.d.ts +13 -1
  6. package/dist/core/errors.d.ts.map +1 -1
  7. package/dist/core/errors.js +32 -0
  8. package/dist/core/model-profiles.d.ts +34 -0
  9. package/dist/core/model-profiles.d.ts.map +1 -0
  10. package/dist/core/model-profiles.js +49 -0
  11. package/dist/core/provenance.d.ts +32 -0
  12. package/dist/core/provenance.d.ts.map +1 -0
  13. package/dist/core/provenance.js +71 -0
  14. package/dist/core/provider.d.ts +13 -0
  15. package/dist/core/provider.d.ts.map +1 -1
  16. package/dist/core/route-planner.d.ts +1 -1
  17. package/dist/core/route-planner.d.ts.map +1 -1
  18. package/dist/core/route-planner.js +2 -1
  19. package/dist/core/router.d.ts +4 -3
  20. package/dist/core/router.d.ts.map +1 -1
  21. package/dist/core/router.js +4 -3
  22. package/dist/facade/text.d.ts +123 -0
  23. package/dist/facade/text.d.ts.map +1 -0
  24. package/dist/facade/text.js +234 -0
  25. package/dist/index.d.ts +36 -45
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +28 -38
  28. package/dist/provider.d.ts +34 -0
  29. package/dist/provider.d.ts.map +1 -0
  30. package/dist/provider.js +33 -0
  31. package/dist/providers/onnx/provider.d.ts.map +1 -1
  32. package/dist/providers/onnx/provider.js +1 -0
  33. package/dist/providers/openai/provider.d.ts +41 -11
  34. package/dist/providers/openai/provider.d.ts.map +1 -1
  35. package/dist/providers/openai/provider.js +168 -58
  36. package/dist/web-sdk.d.ts +21 -3
  37. package/dist/web-sdk.d.ts.map +1 -1
  38. package/dist/web-sdk.js +23 -6
  39. package/package.json +7 -3
  40. package/src/core/engine.ts +114 -35
  41. package/src/core/errors.ts +35 -1
  42. package/src/core/model-profiles.ts +70 -0
  43. package/src/core/provenance.ts +90 -0
  44. package/src/core/provider.ts +13 -0
  45. package/src/core/route-planner.ts +3 -1
  46. package/src/core/router.ts +12 -3
  47. package/src/facade/text.ts +349 -0
  48. package/src/index.ts +49 -95
  49. package/src/provider.ts +77 -0
  50. package/src/providers/onnx/provider.ts +1 -0
  51. package/src/providers/openai/provider.ts +232 -68
  52. package/src/web-sdk.ts +47 -10
package/README.md CHANGED
@@ -10,31 +10,58 @@ TypeScript/Web SDK for IndeRun.
10
10
 
11
11
  This package provides the Web SDK entrypoint, the engine core, routing, telemetry, error normalization, the OpenAI-compatible cloud provider, the Web ONNX Runtime provider for developer-supplied local models, and the Web system-model provider for browser-managed on-device models.
12
12
 
13
- The contract types this SDK's own signatures take and return — `TaskRequest`, `TaskResult`, `StreamEvent`, `StreamRunHandle`, `StreamTerminalOutcome`, `IndeRunError`, the host-service and telemetry interfaces, and (from the `/onnx` subpath) `ModelPackage` — are re-exported here, so a fully typed app does not need to depend on `@independo/inderun-contracts` separately. Import that package directly only if you want the JSON Schemas or the `get*ValidationIssues` validators.
13
+ The contract types this SDK's own signatures take and return — `TaskRequest`, `TaskResult`, `StreamEvent`, `StreamRunHandle`, `StreamTerminalOutcome`, `IndeRunError`, `ExecutionProvenance`, the telemetry interfaces, the host-service interfaces (from `/provider`), and (from the `/onnx` subpath) `ModelPackage` — are re-exported here, so a fully typed app does not need to depend on `@independo/inderun-contracts` separately. Import that package directly only if you want the JSON Schemas or the `get*ValidationIssues` validators.
14
+
15
+ ## Entry points
16
+
17
+ | Import | For | Contains |
18
+ | --- | --- | --- |
19
+ | `@independo/inderun-web` | Applications | `generateText` / `streamText`, `createIndeRunWeb`, the advanced task API (`IndeRun`, `run()` / `stream()` / `checkCapabilities()`), model profiles, contract types, `IndeRunException`, telemetry |
20
+ | `@independo/inderun-web/provider` | Provider authors and host integrators | `ProviderAdapter` and the descriptor/capability types, `ProviderRegistry`, `HostServices` and the browser host services, the `create*` error constructors |
21
+ | `@independo/inderun-web/openai`, `/onnx`, `/system-model` | Registering a provider by hand | One provider adapter each |
22
+
23
+ The router, the route planner bindings and the Event Gate are engine internals and are not exported. Moving from 0.3?
24
+ See the [migration guide](https://github.com/independo-gmbh/inderun/blob/main/docs/migration.md).
14
25
 
15
26
  ## Basic Usage
16
27
 
17
28
  ```ts
18
- import { createIndeRunWeb } from "@independo/inderun-web";
29
+ import { createIndeRunWeb, generateText, streamText } from "@independo/inderun-web";
19
30
 
20
31
  const inderun = createIndeRunWeb({
21
32
  openAI: {
22
- model: "gpt-5.2",
33
+ models: ["gpt-5.2"],
23
34
  endpointUrl: "/api/inderun/openai-responses",
24
35
  auth: "none"
25
36
  }
26
37
  });
27
38
 
28
- const result = await inderun.run({
29
- schemaVersion: "1.0",
30
- task: { kind: "text_to_text" },
31
- prompt: "Write a one-sentence summary of IndeRun."
39
+ const { text, provenance } = await generateText(inderun, {
40
+ prompt: "Write a one-sentence summary of IndeRun.",
41
+ execution: { privacy: "local_preferred" }
32
42
  });
43
+
44
+ const stream = await streamText(inderun, { system: "Use plain language.", prompt: "Explain routing." });
45
+ for await (const delta of stream.textStream) process.stdout.write(delta);
46
+ const { finishReason } = await stream.result;
33
47
  ```
34
48
 
49
+ `generateText` and `streamText` take `prompt` or `messages`, an optional `system`, `model` (a
50
+ [model profile](https://github.com/independo-gmbh/inderun/blob/main/docs/architecture/api-layering.md#model-profiles),
51
+ default `"auto"`), the usual sampling options, and an `execution` policy that reuses the canonical
52
+ constraint values. Each call compiles to one `TaskRequest` — `toTaskRequest(options)` shows which —
53
+ and runs through the engine below, so errors, fallback, and cancellation match the advanced API.
54
+
55
+ ## Advanced: the task API
56
+
57
+ Use `TaskRequest` with `run()` / `stream()` when you need the raw event sequence, build requests in code (for example
58
+ with your own `requestId`), or check capabilities before offering an option —
59
+ `await inderun.checkCapabilities()` reports every registered target's live availability. Both layers run through the
60
+ same engine.
61
+
35
62
  ## Streaming
36
63
 
37
- `stream()` returns the run handle, its canonical `StreamEvent` sequence, and a `cancel()` hook:
64
+ For the full event sequence, use the advanced API. `stream()` returns the run handle, its canonical `StreamEvent` sequence, and a `cancel()` hook:
38
65
 
39
66
  ```ts
40
67
  const { handle, events, cancel } = await inderun.stream({
@@ -66,6 +93,9 @@ accept `"stream": true` and emit `text/event-stream` with the Responses event ty
66
93
  response object. Unknown event types are ignored, so an endpoint that emits extra events is
67
94
  fine; one that emits a different vocabulary entirely is not.
68
95
 
96
+ [OpenRouter](https://github.com/independo-gmbh/inderun/blob/main/docs/openrouter.md) is reached this way: point
97
+ `endpointUrl` at its Responses endpoint (through a proxy in browser apps) and list `vendor/model` ids as `models`.
98
+
69
99
  ## Security Model
70
100
 
71
101
  Browser apps should use a proxy endpoint and keep provider credentials server-side. `createIndeRunWeb` rejects direct calls to the public OpenAI Responses endpoint unless `allowDirectOpenAIEndpoint: true` is set for a controlled environment.
@@ -78,10 +108,17 @@ distributed client code — put it behind a trusted backend proxy and point `end
78
108
  ## Advanced: registering the OpenAI provider directly
79
109
 
80
110
  `createIndeRunWeb` wires the OpenAI Responses provider for you. To register it
81
- manually (e.g. alongside other providers), import it from the provider subpath:
111
+ manually (e.g. alongside other providers), import it from the provider subpath and build the engine from
112
+ the `/provider` entry point:
82
113
 
83
114
  ```ts
115
+ import { IndeRun } from "@independo/inderun-web";
84
116
  import { OpenAIResponsesProvider } from "@independo/inderun-web/openai";
117
+ import { ProviderRegistry, createBrowserHostServices } from "@independo/inderun-web/provider";
118
+
119
+ const registry = new ProviderRegistry();
120
+ registry.register(new OpenAIResponsesProvider({ model: "gpt-5.2", endpointUrl: "/api/inderun/openai-responses", auth: "none" }));
121
+ const inderun = new IndeRun(registry, createBrowserHostServices());
85
122
  ```
86
123
 
87
124
  The provider normalizes OpenAI transport and API failures into the IndeRun error
@@ -4,12 +4,28 @@ import type { ProviderCapabilitySnapshot, StreamRun } from "./provider.js";
4
4
  import type { IndeRunApi } from "./generated/inderun-api.js";
5
5
  import { ProviderRegistry } from "./registry.js";
6
6
  import { type TelemetryService } from "./telemetry.js";
7
+ import { type ModelProfiles } from "./model-profiles.js";
7
8
  /**
8
9
  * @deprecated Renamed to `StreamRun`, which is the name the generated
9
10
  * `IndeRunApi` surface and the Swift/Kotlin SDKs use. Kept as an alias so the
10
11
  * rename is not a breaking change.
11
12
  */
12
13
  export type StreamHandleResult = StreamRun;
14
+ /**
15
+ * Optional engine configuration.
16
+ */
17
+ export interface IndeRunOptions {
18
+ /**
19
+ * Telemetry listener. Defaults to the host's telemetry service, then to a no-op.
20
+ */
21
+ telemetryService?: TelemetryService;
22
+ /**
23
+ * Named model profiles requests can route within (`TaskRequest.modelProfile`).
24
+ * Each maps a profile name to an ordered list of target (provider instance) ids.
25
+ * `"auto"` is built in and cannot be redefined. See `docs/architecture/api-layering.md`.
26
+ */
27
+ modelProfiles?: ModelProfiles;
28
+ }
13
29
  /**
14
30
  * Main orchestrator SDK entrypoint class.
15
31
  * Validates tasks, executes routing rules based on request constraints and connectivity,
@@ -20,13 +36,21 @@ export declare class IndeRun implements IndeRunApi {
20
36
  private hostServices;
21
37
  private router;
22
38
  private telemetryService;
39
+ private modelProfiles;
23
40
  /**
24
41
  * Initializes the IndeRun instance.
25
42
  * @param registry - Registry filled with active providers.
26
43
  * @param hostServices - Services wrapping OS interfaces and indicators.
27
- * @param telemetryService - Optional custom telemetry service listener.
44
+ * @param options - Optional telemetry listener and model profiles.
45
+ * @throws {Error} If a model profile is malformed (see {@link validateModelProfiles}).
46
+ */
47
+ constructor(registry: ProviderRegistry, hostServices: HostServices, options?: IndeRunOptions);
48
+ /**
49
+ * Validates the request against the schema and resolves its model profile.
50
+ * Both failures are request validation errors: they are reported before any
51
+ * provider is probed, with the offending fields in `details.validationIssues`.
28
52
  */
29
- constructor(registry: ProviderRegistry, hostServices: HostServices, telemetryService?: TelemetryService);
53
+ private validateRequest;
30
54
  /**
31
55
  * Current time from the host clock, falling back to wall-clock time when the
32
56
  * host does not supply one.
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/core/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,UAAU,EAChB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAmB,0BAA0B,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC5F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAQjD,OAAO,EAAuB,KAAK,gBAAgB,EAAwB,MAAM,gBAAgB,CAAC;AAGlG;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,SAAS,CAAC;AAE3C;;;;GAIG;AACH,qBAAa,OAAQ,YAAW,UAAU;IAWtC,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,YAAY;IAXtB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,gBAAgB,CAAmB;IAE3C;;;;;OAKG;gBAEO,QAAQ,EAAE,gBAAgB,EAC1B,YAAY,EAAE,YAAY,EAClC,gBAAgB,CAAC,EAAE,gBAAgB;IAOrC;;;OAGG;IACH,OAAO,CAAC,GAAG;IAIX;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAuBxB;;;OAGG;IACH,OAAO,CAAC,QAAQ;IAShB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAoBxB;;;;;;;OAOG;IACG,GAAG,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAsHpD;;;;;;;;;;;;;;;;;OAiBG;IACG,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC;IA+PtD;;;OAGG;IACG,iBAAiB,IAAI,OAAO,CAAC,0BAA0B,EAAE,CAAC;CASjE"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/core/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,UAAU,EAChB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAmB,0BAA0B,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC5F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAQjD,OAAO,EAAuB,KAAK,gBAAgB,EAAwB,MAAM,gBAAgB,CAAC;AAElG,OAAO,EAEL,KAAK,aAAa,EAInB,MAAM,qBAAqB,CAAC;AAG7B;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,SAAS,CAAC;AAE3C;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;OAEG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED;;;;GAIG;AACH,qBAAa,OAAQ,YAAW,UAAU;IAatC,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,YAAY;IAbtB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,aAAa,CAAgB;IAErC;;;;;;OAMG;gBAEO,QAAQ,EAAE,gBAAgB,EAC1B,YAAY,EAAE,YAAY,EAClC,OAAO,GAAE,cAAmB;IAS9B;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAkBvB;;;OAGG;IACH,OAAO,CAAC,GAAG;IAIX;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAwBxB;;;OAGG;IACH,OAAO,CAAC,QAAQ;IAShB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAoBxB;;;;;;;OAOG;IACG,GAAG,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IA4HpD;;;;;;;;;;;;;;;;;OAiBG;IACG,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC;IAoRtD;;;OAGG;IACG,iBAAiB,IAAI,OAAO,CAAC,0BAA0B,EAAE,CAAC;CASjE"}
@@ -4,6 +4,8 @@ import { Router } from "./router.js";
4
4
  import { createCapabilityMismatch, createInternal, createUnavailable, toIndeRunException } from "./errors.js";
5
5
  import { NoOpTelemetryService } from "./telemetry.js";
6
6
  import { EventGate } from "./event-gate.js";
7
+ import { AUTO_MODEL_PROFILE, resolveModelProfile, validateModelProfiles } from "./model-profiles.js";
8
+ import { ProvenanceRecorder } from "./provenance.js";
7
9
  /**
8
10
  * Main orchestrator SDK entrypoint class.
9
11
  * Validates tasks, executes routing rules based on request constraints and connectivity,
@@ -14,18 +16,44 @@ export class IndeRun {
14
16
  hostServices;
15
17
  router;
16
18
  telemetryService;
19
+ modelProfiles;
17
20
  /**
18
21
  * Initializes the IndeRun instance.
19
22
  * @param registry - Registry filled with active providers.
20
23
  * @param hostServices - Services wrapping OS interfaces and indicators.
21
- * @param telemetryService - Optional custom telemetry service listener.
24
+ * @param options - Optional telemetry listener and model profiles.
25
+ * @throws {Error} If a model profile is malformed (see {@link validateModelProfiles}).
22
26
  */
23
- constructor(registry, hostServices, telemetryService) {
27
+ constructor(registry, hostServices, options = {}) {
24
28
  this.registry = registry;
25
29
  this.hostServices = hostServices;
26
30
  this.router = new Router(this.registry);
27
31
  this.telemetryService =
28
- telemetryService || this.hostServices.telemetry || new NoOpTelemetryService();
32
+ options.telemetryService || this.hostServices.telemetry || new NoOpTelemetryService();
33
+ this.modelProfiles = options.modelProfiles ?? {};
34
+ validateModelProfiles(this.modelProfiles);
35
+ }
36
+ /**
37
+ * Validates the request against the schema and resolves its model profile.
38
+ * Both failures are request validation errors: they are reported before any
39
+ * provider is probed, with the offending fields in `details.validationIssues`.
40
+ */
41
+ validateRequest(request, runId) {
42
+ const validationIssues = getTaskRequestValidationIssues(request);
43
+ if (validationIssues.length === 0) {
44
+ const resolved = resolveModelProfile(this.modelProfiles, request.modelProfile);
45
+ if (resolved.known) {
46
+ return resolved.profile;
47
+ }
48
+ validationIssues.push({
49
+ path: "/modelProfile",
50
+ message: `unknown model profile "${request.modelProfile}"; configure it in the engine's modelProfiles`
51
+ });
52
+ }
53
+ const message = `Validation failed for TaskRequest: ${validationIssues
54
+ .map((i) => `${i.path} - ${i.message}`)
55
+ .join("; ")}`;
56
+ throw createInternal(message, { runId, details: { validationIssues } });
29
57
  }
30
58
  /**
31
59
  * Current time from the host clock, falling back to wall-clock time when the
@@ -50,6 +78,7 @@ export class IndeRun {
50
78
  rejectedProviders: routeSelection.routePlan.rejectedProviders,
51
79
  fallbackAvailable: providerCount > 1,
52
80
  taskKind: request.task.kind,
81
+ modelProfile: request.modelProfile ?? "auto",
53
82
  constraints: request.constraints ?? null,
54
83
  preferences: request.preferences ?? null,
55
84
  explanation: routeSelection.explanation
@@ -103,21 +132,14 @@ export class IndeRun {
103
132
  async run(request) {
104
133
  const startTime = this.now();
105
134
  const runId = request.requestId || `run_${Math.random().toString(36).substring(2, 11)}`;
135
+ const provenance = new ProvenanceRecorder(request.modelProfile ?? AUTO_MODEL_PROFILE);
106
136
  try {
107
- // 1. Validate request payload using schema contracts
108
- const validationIssues = getTaskRequestValidationIssues(request);
109
- if (validationIssues.length > 0) {
110
- const message = `Validation failed for TaskRequest: ${validationIssues
111
- .map((i) => `${i.path} - ${i.message}`)
112
- .join("; ")}`;
113
- throw createInternal(message, {
114
- runId,
115
- details: { validationIssues }
116
- });
117
- }
118
- // 2. Select the route based on constraints and host capabilities
119
- const routeSelection = await this.router.selectRoute(request, this.hostServices, "run");
137
+ // 1. Validate request payload using schema contracts and resolve its model profile
138
+ const modelProfile = this.validateRequest(request, runId);
139
+ // 2. Select the route based on constraints, model profile, and host capabilities
140
+ const routeSelection = await this.router.selectRoute(request, this.hostServices, "run", modelProfile);
120
141
  const providers = [routeSelection.provider, ...routeSelection.fallbackProviders];
142
+ provenance.routed(routeSelection.routePlan);
121
143
  this.emitRouteDecided(runId, request, routeSelection, providers.length);
122
144
  // 3. Execute the run task using the planned provider chain
123
145
  const attemptedProviderIds = [];
@@ -138,6 +160,8 @@ export class IndeRun {
138
160
  providerUsed: providerId,
139
161
  totalMs
140
162
  };
163
+ provenance.attempted(providerId, "succeeded");
164
+ result.provenance = provenance.build(provider.describe(), result.telemetry.reportedModel);
141
165
  this.safeEmit({
142
166
  type: "attempt_succeeded",
143
167
  runId,
@@ -152,7 +176,7 @@ export class IndeRun {
152
176
  return result;
153
177
  }
154
178
  catch (err) {
155
- lastError = toIndeRunException(err, {
179
+ const attemptError = toIndeRunException(err, {
156
180
  providerId,
157
181
  runId,
158
182
  details: {
@@ -161,6 +185,8 @@ export class IndeRun {
161
185
  routePlan: routeSelection.routePlan
162
186
  }
163
187
  });
188
+ provenance.attempted(providerId, "failed", attemptError.errorClass);
189
+ lastError = attemptError;
164
190
  }
165
191
  }
166
192
  const endTime = this.now();
@@ -170,14 +196,14 @@ export class IndeRun {
170
196
  details: {
171
197
  totalMs,
172
198
  attemptedProviderIds,
173
- fallbackOccurred: providers.length > 1,
199
+ fallbackOccurred: attemptedProviderIds.length > 1,
174
200
  routePlan: routeSelection.routePlan
175
201
  }
176
202
  });
177
203
  throw exception;
178
204
  }
179
205
  catch (error) {
180
- const exception = toIndeRunException(error, {
206
+ const normalized = toIndeRunException(error, {
181
207
  runId,
182
208
  details: {
183
209
  totalMs: typeof error.details?.totalMs === "number"
@@ -185,6 +211,10 @@ export class IndeRun {
185
211
  : this.now() - startTime
186
212
  }
187
213
  });
214
+ // A routing refusal throws before any attempt; its rejections travel on the details.
215
+ provenance.routingFailed(normalized.details);
216
+ // No attempt produced the result, so the outcome is attributed to no provider.
217
+ const exception = normalized.withProvenance(provenance.build());
188
218
  const endTime = this.now();
189
219
  const totalMs = typeof exception.details?.totalMs === "number"
190
220
  ? exception.details?.totalMs
@@ -225,24 +255,28 @@ export class IndeRun {
225
255
  const startTime = this.now();
226
256
  const now = () => this.now();
227
257
  const runId = request.requestId || `run_${Math.random().toString(36).substring(2, 11)}`;
228
- const validationIssues = getTaskRequestValidationIssues(request);
229
- if (validationIssues.length > 0) {
230
- const message = `Validation failed for TaskRequest: ${validationIssues
231
- .map((i) => `${i.path} - ${i.message}`)
232
- .join("; ")}`;
233
- throw createInternal(message, { runId, details: { validationIssues } });
258
+ const provenance = new ProvenanceRecorder(request.modelProfile ?? AUTO_MODEL_PROFILE);
259
+ let modelProfile;
260
+ try {
261
+ modelProfile = this.validateRequest(request, runId);
262
+ }
263
+ catch (error) {
264
+ throw toIndeRunException(error, { runId }).withProvenance(provenance.build());
234
265
  }
235
266
  // Route selection failures are the one class of stream error that surfaces as a
236
267
  // rejected promise instead of a terminal event, so they must still carry the
237
268
  // runId — there is no handle for the caller to correlate them with otherwise.
238
269
  let routeSelection;
239
270
  try {
240
- routeSelection = await this.router.selectRoute(request, this.hostServices, "stream");
271
+ routeSelection = await this.router.selectRoute(request, this.hostServices, "stream", modelProfile);
241
272
  }
242
273
  catch (error) {
243
- throw toIndeRunException(error, { runId });
274
+ const exception = toIndeRunException(error, { runId });
275
+ provenance.routingFailed(exception.details);
276
+ throw exception.withProvenance(provenance.build());
244
277
  }
245
278
  const providers = [routeSelection.provider, ...routeSelection.fallbackProviders];
279
+ provenance.routed(routeSelection.routePlan);
246
280
  this.emitRouteDecided(runId, request, routeSelection, providers.length);
247
281
  // Defensive only: streaming eligibility is decided by the route planner, which
248
282
  // throws with its own rejection reasons when nothing can stream. Reaching here
@@ -267,14 +301,18 @@ export class IndeRun {
267
301
  async function* run() {
268
302
  const attemptedProviderIds = [];
269
303
  let committed = false;
270
- const cancelledOutcome = (partialText) => {
304
+ const cancelledOutcome = (partialText, cancelledProvider) => {
271
305
  const reason = typeof controller.signal.reason === "string" ? controller.signal.reason : undefined;
306
+ if (cancelledProvider !== undefined) {
307
+ provenance.attempted(cancelledProvider.describe().id, "cancelled");
308
+ }
272
309
  return {
273
310
  outcome: "cancelled",
274
311
  runId,
275
312
  schemaVersion: "1.0",
276
313
  partialText,
277
- ...(reason !== undefined ? { reason } : {})
314
+ ...(reason !== undefined ? { reason } : {}),
315
+ provenance: provenance.build(cancelledProvider?.describe())
278
316
  };
279
317
  };
280
318
  for (const [index, provider] of providers.entries()) {
@@ -325,6 +363,7 @@ export class IndeRun {
325
363
  continue;
326
364
  }
327
365
  // ev.kind === "done"
366
+ provenance.attempted(providerId, "succeeded");
328
367
  const outcome = {
329
368
  outcome: "completed",
330
369
  runId,
@@ -332,7 +371,8 @@ export class IndeRun {
332
371
  finalText: ev.finalText,
333
372
  telemetry: { providerUsed: providerId, totalMs: now() - startTime },
334
373
  ...(ev.finishReason !== undefined ? { finishReason: ev.finishReason } : {}),
335
- ...(ev.usage !== undefined ? { usage: ev.usage } : {})
374
+ ...(ev.usage !== undefined ? { usage: ev.usage } : {}),
375
+ provenance: provenance.build(provider.describe(), ev.reportedModel)
336
376
  };
337
377
  const terminal = gate.terminate(outcome, now());
338
378
  safeEmit({
@@ -350,7 +390,7 @@ export class IndeRun {
350
390
  return;
351
391
  }
352
392
  if (controller.signal.aborted) {
353
- const terminal = gate.terminate(cancelledOutcome(partialText), now());
393
+ const terminal = gate.terminate(cancelledOutcome(partialText, provider), now());
354
394
  safeEmit({
355
395
  type: "stream_cancelled",
356
396
  runId,
@@ -366,7 +406,7 @@ export class IndeRun {
366
406
  }
367
407
  catch (err) {
368
408
  if (controller.signal.aborted) {
369
- const terminal = gate.terminate(cancelledOutcome(partialText), now());
409
+ const terminal = gate.terminate(cancelledOutcome(partialText, provider), now());
370
410
  safeEmit({
371
411
  type: "stream_cancelled",
372
412
  runId,
@@ -382,6 +422,7 @@ export class IndeRun {
382
422
  runId,
383
423
  details: { attemptedProviderIds }
384
424
  });
425
+ provenance.attempted(providerId, "failed", exception.errorClass);
385
426
  if (!committed) {
386
427
  safeEmit({
387
428
  type: "stream_attempt_failed",
@@ -395,12 +436,14 @@ export class IndeRun {
395
436
  });
396
437
  continue;
397
438
  }
439
+ // Committed: the failure belongs to this provider, not to the chain.
398
440
  const outcome = {
399
441
  outcome: "error",
400
442
  runId,
401
443
  schemaVersion: "1.0",
402
444
  error: exception.toContractError(),
403
- partialText
445
+ partialText,
446
+ provenance: provenance.build(provider.describe())
404
447
  };
405
448
  const terminal = gate.terminate(outcome, now());
406
449
  safeEmit({
@@ -441,7 +484,8 @@ export class IndeRun {
441
484
  runId,
442
485
  schemaVersion: "1.0",
443
486
  error: exception.toContractError(),
444
- partialText: ""
487
+ partialText: "",
488
+ provenance: provenance.build()
445
489
  };
446
490
  const terminal = gate.terminate(outcome, now());
447
491
  safeEmit({
@@ -1,4 +1,4 @@
1
- import type { IndeRunError, IndeRunErrorClass } from "@independo/inderun-contracts";
1
+ import type { ExecutionProvenance, IndeRunError, IndeRunErrorClass } from "@independo/inderun-contracts";
2
2
  /**
3
3
  * Parameter structure required to initialize an IndeRunException.
4
4
  */
@@ -31,6 +31,10 @@ export interface IndeRunExceptionParams {
31
31
  * Rich contextual metadata regarding the error.
32
32
  */
33
33
  details?: Record<string, unknown>;
34
+ /**
35
+ * How the failed execution played out, attached by the engine.
36
+ */
37
+ provenance?: ExecutionProvenance;
34
38
  }
35
39
  /**
36
40
  * Standardized framework exception thrown when route selection or provider execution fails.
@@ -51,11 +55,19 @@ export declare class IndeRunException extends Error {
51
55
  readonly retryAfterMs?: number;
52
56
  /** Custom details and metadata context. */
53
57
  readonly details?: Record<string, unknown>;
58
+ /** How the failed execution played out: attempts, rejections, the provider committed to. */
59
+ readonly provenance?: ExecutionProvenance;
54
60
  constructor(params: IndeRunExceptionParams);
55
61
  /**
56
62
  * Converts the exception into a JSON-compatible contract-compliant IndeRunError.
57
63
  */
58
64
  toContractError(): IndeRunError;
65
+ /**
66
+ * A copy of this exception carrying `provenance`. The engine attaches provenance once it
67
+ * knows how the whole execution ended, replacing anything set earlier.
68
+ */
69
+ withProvenance(provenance: ExecutionProvenance): IndeRunException;
70
+ private params;
59
71
  }
60
72
  /**
61
73
  * Factory helper for CapabilityMismatch error class.
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AAEpF;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;OAEG;IACH,UAAU,EAAE,iBAAiB,CAAC;IAC9B;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,qDAAqD;IACrD,QAAQ,CAAC,aAAa,SAAS;IAC/B,sCAAsC;IACtC,QAAQ,CAAC,UAAU,EAAE,iBAAiB,CAAC;IACvC,mCAAmC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,iDAAiD;IACjD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,oDAAoD;IACpD,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAC7B,gEAAgE;IAChE,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,2CAA2C;IAC3C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBAE/B,MAAM,EAAE,sBAAsB;IAa1C;;OAEG;IACH,eAAe,IAAI,YAAY;CAahC;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,YAAY,GAAG,SAAS,CAAC,GAC7D,gBAAgB,CAElB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,YAAY,GAAG,SAAS,CAAC,GAC7D,gBAAgB,CAElB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,YAAY,GAAG,SAAS,CAAC,GAC7D,gBAAgB,CAElB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,YAAY,GAAG,SAAS,CAAC,GAC7D,gBAAgB,CAElB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,YAAY,GAAG,SAAS,CAAC,GAC7D,gBAAgB,CAElB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,YAAY,GAAG,SAAS,CAAC,GAC7D,gBAAgB,CAElB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,YAAY,GAAG,SAAS,CAAC,GAC7D,gBAAgB,CAElB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,OAAO,EACd,cAAc,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,sBAAsB,EAAE,SAAS,CAAC,CAAC,GAChE,gBAAgB,CAuDlB"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"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"}
@@ -17,6 +17,8 @@ export class IndeRunException extends Error {
17
17
  retryAfterMs;
18
18
  /** Custom details and metadata context. */
19
19
  details;
20
+ /** How the failed execution played out: attempts, rejections, the provider committed to. */
21
+ provenance;
20
22
  constructor(params) {
21
23
  super(params.message);
22
24
  this.name = "IndeRunException";
@@ -31,6 +33,8 @@ export class IndeRunException extends Error {
31
33
  this.retryAfterMs = params.retryAfterMs;
32
34
  if (params.details !== undefined)
33
35
  this.details = params.details;
36
+ if (params.provenance !== undefined)
37
+ this.provenance = params.provenance;
34
38
  Object.setPrototypeOf(this, IndeRunException.prototype);
35
39
  }
36
40
  /**
@@ -52,8 +56,33 @@ export class IndeRunException extends Error {
52
56
  err.retryAfterMs = this.retryAfterMs;
53
57
  if (this.details !== undefined)
54
58
  err.details = this.details;
59
+ if (this.provenance !== undefined)
60
+ err.provenance = this.provenance;
55
61
  return err;
56
62
  }
63
+ /**
64
+ * A copy of this exception carrying `provenance`. The engine attaches provenance once it
65
+ * knows how the whole execution ended, replacing anything set earlier.
66
+ */
67
+ withProvenance(provenance) {
68
+ return new IndeRunException({ ...this.params(), provenance });
69
+ }
70
+ params() {
71
+ const params = { errorClass: this.errorClass, message: this.message };
72
+ if (this.runId !== undefined)
73
+ params.runId = this.runId;
74
+ if (this.providerId !== undefined)
75
+ params.providerId = this.providerId;
76
+ if (this.retryable !== undefined)
77
+ params.retryable = this.retryable;
78
+ if (this.retryAfterMs !== undefined)
79
+ params.retryAfterMs = this.retryAfterMs;
80
+ if (this.details !== undefined)
81
+ params.details = this.details;
82
+ if (this.provenance !== undefined)
83
+ params.provenance = this.provenance;
84
+ return params;
85
+ }
57
86
  }
58
87
  /**
59
88
  * Factory helper for CapabilityMismatch error class.
@@ -126,6 +155,9 @@ export function toIndeRunException(error, fallbackParams) {
126
155
  ...(fallbackParams.details || {})
127
156
  };
128
157
  }
158
+ const provenance = error.provenance ?? fallbackParams.provenance;
159
+ if (provenance !== undefined)
160
+ mergedParams.provenance = provenance;
129
161
  return new IndeRunException(mergedParams);
130
162
  }
131
163
  return error;
@@ -0,0 +1,34 @@
1
+ import type { SharedPlannerInput } from "./route-planner.js";
2
+ /**
3
+ * The built-in profile: every registered target, ranked by constraints and
4
+ * preferences alone. Requests that name no profile route within it.
5
+ */
6
+ export declare const AUTO_MODEL_PROFILE = "auto";
7
+ /**
8
+ * Application-defined model profiles: a profile name mapped to an ordered list of
9
+ * target ids (provider instance ids, each bound to one model). Requests name a
10
+ * profile through `TaskRequest.modelProfile`; the route planner treats the list as
11
+ * an allow-list and ranks its entries after placement and before preferences.
12
+ * Ids that name no registered provider are ignored by the planner.
13
+ */
14
+ export type ModelProfiles = Readonly<Record<string, readonly string[]>>;
15
+ /** A resolved profile as the shared planner consumes it; absent means `auto`. */
16
+ export type ResolvedModelProfile = SharedPlannerInput["modelProfile"];
17
+ /**
18
+ * Checks profile configuration once, when the engine is constructed, so a
19
+ * malformed profile is a setup error rather than a per-request surprise.
20
+ * @throws {Error} When a profile is named `auto`, is empty, or lists a target twice.
21
+ */
22
+ export declare function validateModelProfiles(profiles: ModelProfiles): void;
23
+ /**
24
+ * Resolves a request's profile name against the configured profiles.
25
+ * @returns `{ known: true }` with the planner input (undefined for `auto`), or
26
+ * `{ known: false }` when the name matches no configured profile.
27
+ */
28
+ export declare function resolveModelProfile(profiles: ModelProfiles, name: string | undefined): {
29
+ known: true;
30
+ profile: ResolvedModelProfile;
31
+ } | {
32
+ known: false;
33
+ };
34
+ //# sourceMappingURL=model-profiles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-profiles.d.ts","sourceRoot":"","sources":["../../src/core/model-profiles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAE7D;;;GAGG;AACH,eAAO,MAAM,kBAAkB,SAAS,CAAC;AAEzC;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;AAExE,iFAAiF;AACjF,MAAM,MAAM,oBAAoB,GAAG,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAEtE;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,CAwBnE;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,aAAa,EACvB,IAAI,EAAE,MAAM,GAAG,SAAS,GACvB;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,oBAAoB,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,CAUnE"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The built-in profile: every registered target, ranked by constraints and
3
+ * preferences alone. Requests that name no profile route within it.
4
+ */
5
+ export const AUTO_MODEL_PROFILE = "auto";
6
+ /**
7
+ * Checks profile configuration once, when the engine is constructed, so a
8
+ * malformed profile is a setup error rather than a per-request surprise.
9
+ * @throws {Error} When a profile is named `auto`, is empty, or lists a target twice.
10
+ */
11
+ export function validateModelProfiles(profiles) {
12
+ for (const [name, targetIds] of Object.entries(profiles)) {
13
+ if (name.length === 0) {
14
+ throw new Error("Model profile names must be non-empty.");
15
+ }
16
+ if (name === AUTO_MODEL_PROFILE) {
17
+ throw new Error(`"${AUTO_MODEL_PROFILE}" is the built-in model profile and cannot be redefined.`);
18
+ }
19
+ if (targetIds.length === 0) {
20
+ throw new Error(`Model profile "${name}" must list at least one target id.`);
21
+ }
22
+ const seen = new Set();
23
+ for (const id of targetIds) {
24
+ if (id.length === 0) {
25
+ throw new Error(`Model profile "${name}" lists an empty target id.`);
26
+ }
27
+ if (seen.has(id)) {
28
+ throw new Error(`Model profile "${name}" lists target "${id}" more than once.`);
29
+ }
30
+ seen.add(id);
31
+ }
32
+ }
33
+ }
34
+ /**
35
+ * Resolves a request's profile name against the configured profiles.
36
+ * @returns `{ known: true }` with the planner input (undefined for `auto`), or
37
+ * `{ known: false }` when the name matches no configured profile.
38
+ */
39
+ export function resolveModelProfile(profiles, name) {
40
+ if (name === undefined || name === AUTO_MODEL_PROFILE) {
41
+ return { known: true, profile: undefined };
42
+ }
43
+ if (!Object.hasOwn(profiles, name)) {
44
+ return { known: false };
45
+ }
46
+ // validateModelProfiles guarantees every configured profile is non-empty.
47
+ const [first, ...rest] = profiles[name];
48
+ return { known: true, profile: { name, targetIds: [first, ...rest] } };
49
+ }