@independo/inderun-web 0.3.3-rc.2 → 0.4.0-rc.1

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 (47) hide show
  1. package/README.md +16 -7
  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 +12 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +10 -0
  28. package/dist/providers/onnx/provider.d.ts.map +1 -1
  29. package/dist/providers/onnx/provider.js +1 -0
  30. package/dist/providers/openai/provider.d.ts.map +1 -1
  31. package/dist/providers/openai/provider.js +18 -0
  32. package/dist/web-sdk.d.ts +19 -2
  33. package/dist/web-sdk.d.ts.map +1 -1
  34. package/dist/web-sdk.js +10 -2
  35. package/package.json +3 -3
  36. package/src/core/engine.ts +114 -35
  37. package/src/core/errors.ts +35 -1
  38. package/src/core/model-profiles.ts +70 -0
  39. package/src/core/provenance.ts +90 -0
  40. package/src/core/provider.ts +13 -0
  41. package/src/core/route-planner.ts +3 -1
  42. package/src/core/router.ts +12 -3
  43. package/src/facade/text.ts +349 -0
  44. package/src/index.ts +28 -2
  45. package/src/providers/onnx/provider.ts +1 -0
  46. package/src/providers/openai/provider.ts +21 -0
  47. package/src/web-sdk.ts +31 -5
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/providers/openai/provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAQ5E,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,oBAAoB,CAAC;AACpE,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EAClB,2BAA2B,EAC3B,qBAAqB,EACrB,mBAAmB,EACnB,UAAU,EACX,MAAM,wBAAwB,CAAC;AA4BhC,eAAO,MAAM,iCAAiC,wCAAwC,CAAC;AAKvF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;OAEG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,IAAI,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAAC;IACjC;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AA6BD;;;;;;GAMG;AACH,qBAAa,uBAAwB,YAAW,eAAe;IAUjD,OAAO,CAAC,QAAQ,CAAC,OAAO;IATpC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA4B;IACjD,OAAO,CAAC,YAAY,CAAC,CAA6D;IAElF;;;OAGG;gBAC0B,OAAO,EAAE,qBAAqB;IAM3D;;OAEG;IACH,QAAQ,IAAI,kBAAkB;IAuB9B;;;;;;;;OAQG;IACG,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,2BAA2B,CAAC;YA+B9D,sBAAsB;IAuCpC;;;;;;OAMG;IACG,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IAiFzE;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACI,MAAM,CACX,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAAC,mBAAmB,CAAC;IAiGtC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,cAAc;IAmBtB;;;;;OAKG;YACW,cAAc;IA2C5B,OAAO,CAAC,iBAAiB;IA2BzB;;;;;;;OAOG;IACH,OAAO,CAAC,YAAY;CAmErB"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/providers/openai/provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAQ5E,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,oBAAoB,CAAC;AACpE,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EAClB,2BAA2B,EAC3B,qBAAqB,EACrB,mBAAmB,EACnB,UAAU,EACX,MAAM,wBAAwB,CAAC;AA4BhC,eAAO,MAAM,iCAAiC,wCAAwC,CAAC;AAKvF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;OAEG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,IAAI,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAAC;IACjC;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AA8BD;;;;;;GAMG;AACH,qBAAa,uBAAwB,YAAW,eAAe;IAUjD,OAAO,CAAC,QAAQ,CAAC,OAAO;IATpC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA4B;IACjD,OAAO,CAAC,YAAY,CAAC,CAA6D;IAElF;;;OAGG;gBAC0B,OAAO,EAAE,qBAAqB;IAM3D;;OAEG;IACH,QAAQ,IAAI,kBAAkB;IAwB9B;;;;;;;;OAQG;IACG,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,2BAA2B,CAAC;YA+B9D,sBAAsB;IAuCpC;;;;;;OAMG;IACG,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IAsFzE;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACI,MAAM,CACX,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAAC,mBAAmB,CAAC;IAqGtC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,cAAc;IAmBtB;;;;;OAKG;YACW,cAAc;IA2C5B,OAAO,CAAC,iBAAiB;IA2BzB;;;;;;;OAOG;IACH,OAAO,CAAC,YAAY;CAmErB"}
@@ -42,6 +42,7 @@ export class OpenAIResponsesProvider {
42
42
  return {
43
43
  id: this.id,
44
44
  type: "cloud",
45
+ model: this.options.model,
45
46
  transport: "http",
46
47
  streamingStyle: "tokens",
47
48
  supports: {
@@ -193,6 +194,10 @@ export class OpenAIResponsesProvider {
193
194
  totalMs: 0
194
195
  }
195
196
  };
197
+ const reportedModel = getReportedModel(responseBody);
198
+ if (reportedModel !== undefined) {
199
+ result.telemetry.reportedModel = reportedModel;
200
+ }
196
201
  const usage = getUsage(responseBody);
197
202
  if (usage) {
198
203
  result.usage = usage;
@@ -286,6 +291,10 @@ export class OpenAIResponsesProvider {
286
291
  if (usage) {
287
292
  done.usage = usage;
288
293
  }
294
+ const reportedModel = getReportedModel(body);
295
+ if (reportedModel !== undefined) {
296
+ done.reportedModel = reportedModel;
297
+ }
289
298
  yield done;
290
299
  return;
291
300
  }
@@ -485,6 +494,15 @@ function getFinishReason(response) {
485
494
  }
486
495
  return "stop";
487
496
  }
497
+ /**
498
+ * The model id the Responses API echoes on the response object. It names the model that
499
+ * served the request, which can be a dated snapshot of the configured alias.
500
+ */
501
+ function getReportedModel(responseBody) {
502
+ return typeof responseBody.model === "string" && responseBody.model.length > 0
503
+ ? responseBody.model
504
+ : undefined;
505
+ }
488
506
  function getUsage(response) {
489
507
  const usage = {};
490
508
  if (typeof response.usage?.input_tokens === "number") {
package/dist/web-sdk.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type CreateBrowserHostServicesOptions } from "./core/browser-host.js";
2
2
  import { IndeRun } from "./core/engine.js";
3
+ import type { ModelProfiles } from "./core/model-profiles.js";
3
4
  import { type OnnxProviderOptions } from "./providers/onnx/provider.js";
4
5
  import { type OpenAIProviderOptions } from "./providers/openai/provider.js";
5
6
  import { type SystemModelProviderOptions } from "./providers/system-model/provider.js";
@@ -8,9 +9,11 @@ import { type SystemModelProviderOptions } from "./providers/system-model/provid
8
9
  */
9
10
  export interface CreateIndeRunWebOptions {
10
11
  /**
11
- * OpenAI Responses provider configuration registered by the factory.
12
+ * OpenAI Responses provider configuration registered by the factory. One target is
13
+ * registered per entry in `models`, with id `<id>:<model>` (`id` defaults to
14
+ * `"openai"`), so model profiles can list each model separately.
12
15
  */
13
- openAI?: OpenAIProviderOptions;
16
+ openAI?: OpenAIFactoryOptions;
14
17
  /**
15
18
  * Web ONNX Runtime provider configuration registered by the factory, for developer-supplied
16
19
  * local models. Registering it is what makes `constraints.privacy = "local_required"` routable.
@@ -34,7 +37,21 @@ export interface CreateIndeRunWebOptions {
34
37
  * non-production environments.
35
38
  */
36
39
  allowDirectOpenAIEndpoint?: boolean;
40
+ /**
41
+ * Named model profiles requests can route within. Each maps a profile name to an
42
+ * ordered list of target ids, for example `{ smart: ["openai:gpt-5.2"] }`.
43
+ */
44
+ modelProfiles?: ModelProfiles;
37
45
  }
46
+ /**
47
+ * OpenAI options for {@link createIndeRunWeb}: the provider options with `models` in
48
+ * place of the single `model`. The first model is not special; every model becomes
49
+ * its own target.
50
+ */
51
+ export type OpenAIFactoryOptions = Omit<OpenAIProviderOptions, "model"> & {
52
+ /** OpenAI model ids to register, one target each. */
53
+ models: readonly string[];
54
+ };
38
55
  /**
39
56
  * Creates a Web SDK instance with the configured providers registered.
40
57
  *
@@ -1 +1 @@
1
- {"version":3,"file":"web-sdk.d.ts","sourceRoot":"","sources":["../src/web-sdk.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,gCAAgC,EACtC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAA0B,KAAK,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAChG,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAEL,KAAK,0BAA0B,EAChC,MAAM,sCAAsC,CAAC;AAE9C;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC;;OAEG;IACH,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAC/B;;;OAGG;IACH,IAAI,CAAC,EAAE,mBAAmB,CAAC;IAC3B;;;;OAIG;IACH,WAAW,CAAC,EAAE,0BAA0B,CAAC;IACzC;;OAEG;IACH,YAAY,CAAC,EAAE,gCAAgC,CAAC;IAChD;;;;;;OAMG;IACH,yBAAyB,CAAC,EAAE,OAAO,CAAC;CACrC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAuB1E"}
1
+ {"version":3,"file":"web-sdk.d.ts","sourceRoot":"","sources":["../src/web-sdk.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,gCAAgC,EACtC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAA0B,KAAK,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAChG,OAAO,EAGL,KAAK,qBAAqB,EAC3B,MAAM,gCAAgC,CAAC;AAExC,OAAO,EAEL,KAAK,0BAA0B,EAChC,MAAM,sCAAsC,CAAC;AAE9C;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B;;;OAGG;IACH,IAAI,CAAC,EAAE,mBAAmB,CAAC;IAC3B;;;;OAIG;IACH,WAAW,CAAC,EAAE,0BAA0B,CAAC;IACzC;;OAEG;IACH,YAAY,CAAC,EAAE,gCAAgC,CAAC;IAChD;;;;;;OAMG;IACH,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC;;;OAGG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,qBAAqB,EAAE,OAAO,CAAC,GAAG;IACxE,qDAAqD;IACrD,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3B,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CA+B1E"}
package/dist/web-sdk.js CHANGED
@@ -24,7 +24,13 @@ export function createIndeRunWeb(options) {
24
24
  const registry = new ProviderRegistry();
25
25
  if (options.openAI) {
26
26
  assertSafeOpenAIEndpoint(options.openAI, options.allowDirectOpenAIEndpoint);
27
- registry.register(new OpenAIResponsesProvider(options.openAI));
27
+ const { models, id = "openai", ...shared } = options.openAI;
28
+ if (models.length === 0) {
29
+ throw new Error("createIndeRunWeb requires openAI.models to list at least one model.");
30
+ }
31
+ for (const model of models) {
32
+ registry.register(new OpenAIResponsesProvider({ ...shared, id: `${id}:${model}`, model }));
33
+ }
28
34
  }
29
35
  if (options.onnx) {
30
36
  registry.register(new OnnxRuntimeWebProvider(options.onnx));
@@ -32,7 +38,9 @@ export function createIndeRunWeb(options) {
32
38
  if (options.systemModel) {
33
39
  registry.register(new SystemModelWebProvider(options.systemModel));
34
40
  }
35
- return new IndeRun(registry, createBrowserHostServices(options.hostServices));
41
+ return new IndeRun(registry, createBrowserHostServices(options.hostServices), {
42
+ ...(options.modelProfiles ? { modelProfiles: options.modelProfiles } : {})
43
+ });
36
44
  }
37
45
  function assertSafeOpenAIEndpoint(openAI, allowDirectOpenAIEndpoint) {
38
46
  const endpointUrl = openAI.endpointUrl ?? DEFAULT_OPENAI_RESPONSES_ENDPOINT;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@independo/inderun-web",
3
- "version": "0.3.3-rc.2",
3
+ "version": "0.4.0-rc.1",
4
4
  "description": "On-device AI with automatic cloud fallback for web apps. Runs ONNX Runtime or the Chrome Prompt API in the browser and falls back to an OpenAI-compatible endpoint, with streaming and cancellation behind one API.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -68,8 +68,8 @@
68
68
  "README.md"
69
69
  ],
70
70
  "dependencies": {
71
- "@independo/inderun-contracts": "^0.3.3-rc.2",
72
- "@independo/inderun-route-core-wasm": "^0.3.3-rc.2"
71
+ "@independo/inderun-contracts": "^0.4.0-rc.1",
72
+ "@independo/inderun-route-core-wasm": "^0.4.0-rc.1"
73
73
  },
74
74
  "peerDependencies": {
75
75
  "@huggingface/transformers": "^4.0.0"
@@ -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 telemetryService - Optional custom telemetry service listener.
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
- telemetryService?: TelemetryService
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 validationIssues = getTaskRequestValidationIssues(request);
144
- if (validationIssues.length > 0) {
145
- const message = `Validation failed for TaskRequest: ${validationIssues
146
- .map((i) => `${i.path} - ${i.message}`)
147
- .join("; ")}`;
148
- throw createInternal(message, {
149
- runId,
150
- details: { validationIssues }
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
- lastError = toIndeRunException(err, {
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: providers.length > 1,
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 exception = toIndeRunException(error, {
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
- const validationIssues = getTaskRequestValidationIssues(request);
278
- if (validationIssues.length > 0) {
279
- const message = `Validation failed for TaskRequest: ${validationIssues
280
- .map((i) => `${i.path} - ${i.message}`)
281
- .join("; ")}`;
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(request, this.hostServices, "stream");
348
+ routeSelection = await this.router.selectRoute(
349
+ request,
350
+ this.hostServices,
351
+ "stream",
352
+ modelProfile
353
+ );
291
354
  } catch (error) {
292
- throw toIndeRunException(error, { runId });
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 = (partialText: string): StreamTerminalOutcome => {
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({
@@ -1,4 +1,8 @@
1
- import type { IndeRunError, IndeRunErrorClass } from "@independo/inderun-contracts";
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
+ }