@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
@@ -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: {
@@ -173,7 +174,15 @@ export class OpenAIResponsesProvider {
173
174
  throw this.mapHttpError(response.status, response.statusText, response.headers, response.body, context);
174
175
  }
175
176
  const responseBody = parseJson(response.body);
177
+ // A lenient server (OpenRouter among them) can answer 2xx for a run that
178
+ // failed upstream; that is a failure to classify, not an empty answer.
179
+ if (responseBody.status === "failed" || responseBody.status === "cancelled") {
180
+ throw this.mapReportedError(responseBody.error, `OpenAI Responses run ended with status '${responseBody.status}'.`, context, responseBody.error_type);
181
+ }
176
182
  const outputText = extractOutputText(responseBody);
183
+ if (outputText === undefined && isRecord(responseBody.error)) {
184
+ throw this.mapReportedError(responseBody.error, "OpenAI Responses payload reported an error.", context, responseBody.error_type);
185
+ }
177
186
  if (outputText === undefined) {
178
187
  throw createInternal("OpenAI Responses payload did not contain text output.", {
179
188
  providerId: this.id,
@@ -193,6 +202,10 @@ export class OpenAIResponsesProvider {
193
202
  totalMs: 0
194
203
  }
195
204
  };
205
+ const reportedModel = getReportedModel(responseBody);
206
+ if (reportedModel !== undefined) {
207
+ result.telemetry.reportedModel = reportedModel;
208
+ }
196
209
  const usage = getUsage(responseBody);
197
210
  if (usage) {
198
211
  result.usage = usage;
@@ -216,7 +229,9 @@ export class OpenAIResponsesProvider {
216
229
  * - `response.completed` and `response.incomplete` become the terminal done,
217
230
  * with `finalText`, `usage`, and `finishReason` read off the embedded
218
231
  * response object by the same helpers Mode 1 uses — it has the same shape;
219
- * - `response.failed` and `error` become a terminal error;
232
+ * - `response.failed`, `response.error` and `error` become a terminal error,
233
+ * as does an untyped event carrying an `error` object (OpenRouter's
234
+ * documented mid-stream failure shape);
220
235
  * - every other event type is ignored, since the set is open and additive.
221
236
  *
222
237
  * @param request - Canonical IndeRun task request.
@@ -286,10 +301,17 @@ export class OpenAIResponsesProvider {
286
301
  if (usage) {
287
302
  done.usage = usage;
288
303
  }
304
+ const reportedModel = getReportedModel(body);
305
+ if (reportedModel !== undefined) {
306
+ done.reportedModel = reportedModel;
307
+ }
289
308
  yield done;
290
309
  return;
291
310
  }
292
- if (type === "response.failed" || type === "error") {
311
+ if (type === "response.failed" ||
312
+ type === "response.error" ||
313
+ type === "error" ||
314
+ (type === undefined && isRecord(parsed.error))) {
293
315
  yield { kind: "error", error: this.mapStreamError(parsed, context) };
294
316
  return;
295
317
  }
@@ -299,28 +321,64 @@ export class OpenAIResponsesProvider {
299
321
  // whatever deltas happened to arrive would hide a truncated response.
300
322
  }
301
323
  /**
302
- * Maps a `response.failed` / `error` stream event onto the error taxonomy.
303
- * These arrive over an HTTP 200 body, so there is no status code to classify
304
- * from; OpenAI reports rate limiting and auth failures here with the same
305
- * `code` values it uses in unary error bodies.
324
+ * Maps a `response.failed` / `response.error` / `error` stream event onto the
325
+ * error taxonomy. These arrive over an HTTP 200 body, so there is no status
326
+ * code to classify from; the event's own `code` is classified instead.
306
327
  *
307
328
  * Two payload shapes exist and both are accepted: the standalone `error`
308
329
  * event carries `code`/`message` at its root, while `response.failed` nests
309
- * them under `response.error`. The root `type` is the event name rather than
310
- * an error type, so it is never read as one.
330
+ * them under `response.error` and some servers nest them under `error`. The
331
+ * root `type` is the event name rather than an error type, so it is never
332
+ * read as one. OpenRouter's typed classification comes from the embedded
333
+ * response (`response.failed`) or the error's metadata (untyped events).
311
334
  */
312
335
  mapStreamError(event, context) {
313
336
  const nested = event.error ?? event.response?.error;
314
- const message = nested?.message ?? event.message ?? "OpenAI Responses stream reported a failure.";
315
- const code = nested?.code ?? event.code;
316
- const details = { errorType: nested?.type, errorCode: code };
337
+ return this.mapReportedError({
338
+ message: nested?.message ?? event.message,
339
+ type: nested?.type,
340
+ code: nested?.code ?? event.code
341
+ }, "OpenAI Responses stream reported a failure.", context, event.response?.error_type ?? nested?.metadata?.error_type);
342
+ }
343
+ /**
344
+ * Classifies a failure reported in a body rather than by the HTTP status: a
345
+ * failed stream event or a 2xx run body whose status is `failed`. The most
346
+ * specific signal wins:
347
+ * 1. OpenRouter's typed `error_type`, classified by the HTTP status it
348
+ * documents for that type (see `openRouterErrorTypeStatus`);
349
+ * 2. an integer `code`, which is an HTTP status (OpenRouter's convention) and
350
+ * is classified exactly as `mapHttpError` classifies that status;
351
+ * 3. a string code, by OpenAI's names.
352
+ * Anything unrecognized is a transient `Unavailable`, since the server did
353
+ * accept the request.
354
+ */
355
+ mapReportedError(reported, fallbackMessage, context, errorType) {
356
+ const message = typeof reported?.message === "string" && reported.message.length > 0
357
+ ? reported.message
358
+ : fallbackMessage;
359
+ const typedStatus = openRouterErrorTypeStatus(errorType);
360
+ const details = {
361
+ errorType: typedStatus !== undefined ? errorType : reported?.type,
362
+ errorCode: getErrorCode(reported?.code)
363
+ };
317
364
  const params = { providerId: this.id, runId: context.runId, details };
365
+ if (typedStatus !== undefined) {
366
+ return this.errorForStatus(typedStatus, message, params);
367
+ }
368
+ const code = reported?.code;
369
+ if (typeof code === "number" && Number.isInteger(code)) {
370
+ return this.errorForStatus(code, message, params);
371
+ }
318
372
  switch (code) {
319
373
  case "rate_limit_exceeded":
320
374
  return createRateLimited(message, { ...params, retryable: true });
321
375
  case "invalid_api_key":
322
376
  case "authentication_error":
323
377
  return createAuthError(message, params);
378
+ // The request itself was rejected; repeating it cannot succeed.
379
+ case "invalid_prompt":
380
+ case "image_content_policy_violation":
381
+ return createInternal(message, params);
324
382
  default:
325
383
  return createUnavailable(message, { ...params, retryable: true });
326
384
  }
@@ -386,66 +444,64 @@ export class OpenAIResponsesProvider {
386
444
  return body;
387
445
  }
388
446
  /**
389
- * Maps an OpenAI HTTP error response onto the IndeRun error taxonomy:
390
- * - `401` / `403` → `AuthError`
391
- * - `429` → `RateLimited` (honors `Retry-After` when present)
392
- * - `408` / `504` → `Timeout`
393
- * - `409` / `5xx` → `Unavailable`
394
- * - any other non-2xx status → `Internal`
447
+ * Maps an OpenAI HTTP error response onto the IndeRun error taxonomy by its
448
+ * status (see `errorForStatus`), honoring `Retry-After` on whichever status
449
+ * carries it.
395
450
  */
396
451
  mapHttpError(status, statusText, headers, body, context) {
397
452
  const error = parseJson(body);
398
453
  const message = getOpenAIErrorMessage(error, status, statusText);
399
- const details = {
400
- status,
401
- statusText,
402
- errorType: getString(error.error?.type),
403
- errorCode: getString(error.error?.code)
454
+ const params = {
455
+ providerId: this.id,
456
+ runId: context.runId,
457
+ details: {
458
+ status,
459
+ statusText,
460
+ errorType: getString(error.error?.type),
461
+ errorCode: getErrorCode(error.error?.code)
462
+ }
404
463
  };
464
+ return this.errorForStatus(status, message, params, parseRetryAfterMs(headers));
465
+ }
466
+ /**
467
+ * The status classification shared by HTTP errors and numeric in-body codes:
468
+ * - `401` / `403` → `AuthError`
469
+ * - `402` → `Unavailable`, not retryable: the account is out of credits, so
470
+ * this target cannot serve until someone tops it up. A 402 that carries
471
+ * `Retry-After` is the exception — OpenRouter documents it for its
472
+ * in-flight budget limit, a wait-and-retry case — and is `RateLimited`. Fallback is unaffected
473
+ * either way: the engine moves on to the next target whatever the class.
474
+ * - `429` → `RateLimited`
475
+ * - `408` / `504` → `Timeout`
476
+ * - `409` / `5xx` → `Unavailable`
477
+ * - any other status → `Internal`
478
+ *
479
+ * `retryAfterMs` travels on every retryable class; it is absent for numeric
480
+ * in-body codes, which have no header to read it from.
481
+ */
482
+ errorForStatus(status, message, params, retryAfterMs) {
483
+ const retry = { ...params, retryable: true };
484
+ if (retryAfterMs !== undefined) {
485
+ retry.retryAfterMs = retryAfterMs;
486
+ }
405
487
  if (status === 401 || status === 403) {
406
- return createAuthError(message, {
407
- providerId: this.id,
408
- runId: context.runId,
409
- details
410
- });
488
+ return createAuthError(message, params);
489
+ }
490
+ if (status === 402) {
491
+ return retryAfterMs === undefined
492
+ ? createUnavailable(message, { ...params, retryable: false })
493
+ : createRateLimited(message, retry);
411
494
  }
412
495
  if (status === 429) {
413
- const retryAfterMs = parseRetryAfterMs(headers);
414
- const params = {
415
- providerId: this.id,
416
- runId: context.runId,
417
- retryable: true,
418
- details
419
- };
420
- if (retryAfterMs !== undefined) {
421
- return createRateLimited(message, {
422
- ...params,
423
- retryAfterMs
424
- });
425
- }
426
- return createRateLimited(message, params);
496
+ return createRateLimited(message, retry);
427
497
  }
428
498
  if (status === 408 || status === 504) {
429
- return createTimeout(message, {
430
- providerId: this.id,
431
- runId: context.runId,
432
- retryable: true,
433
- details
434
- });
499
+ return createTimeout(message, retry);
435
500
  }
436
501
  if (status === 409 || status >= 500) {
437
- return createUnavailable(message, {
438
- providerId: this.id,
439
- runId: context.runId,
440
- retryable: true,
441
- details
442
- });
502
+ return createUnavailable(message, retry);
443
503
  }
444
- return createInternal(message, {
445
- providerId: this.id,
446
- runId: context.runId,
447
- details
448
- });
504
+ return createInternal(message, params);
449
505
  }
450
506
  }
451
507
  function createInput(request) {
@@ -485,6 +541,15 @@ function getFinishReason(response) {
485
541
  }
486
542
  return "stop";
487
543
  }
544
+ /**
545
+ * The model id the Responses API echoes on the response object. It names the model that
546
+ * served the request, which can be a dated snapshot of the configured alias.
547
+ */
548
+ function getReportedModel(responseBody) {
549
+ return typeof responseBody.model === "string" && responseBody.model.length > 0
550
+ ? responseBody.model
551
+ : undefined;
552
+ }
488
553
  function getUsage(response) {
489
554
  const usage = {};
490
555
  if (typeof response.usage?.input_tokens === "number") {
@@ -545,3 +610,48 @@ function isRecord(value) {
545
610
  function getString(value) {
546
611
  return typeof value === "string" ? value : undefined;
547
612
  }
613
+ /**
614
+ * The HTTP status OpenRouter documents for each typed `error_type`, so a typed
615
+ * failure is classified exactly like the equivalent HTTP error. Refusals and
616
+ * content-policy violations are documented as 403 but are not credential
617
+ * failures, so they classify as a rejected request (400). Unknown types return
618
+ * `undefined` and the native `code` decides instead.
619
+ */
620
+ function openRouterErrorTypeStatus(errorType) {
621
+ return typeof errorType === "string" && Object.hasOwn(OPENROUTER_ERROR_TYPE_STATUS, errorType)
622
+ ? OPENROUTER_ERROR_TYPE_STATUS[errorType]
623
+ : undefined;
624
+ }
625
+ const OPENROUTER_ERROR_TYPE_STATUS = {
626
+ authentication: 401,
627
+ payment_required: 402,
628
+ permission_denied: 403,
629
+ rate_limit_exceeded: 429,
630
+ timeout: 504,
631
+ provider_unavailable: 502,
632
+ provider_overloaded: 503,
633
+ server: 500,
634
+ unmapped: 500,
635
+ not_found: 404,
636
+ image_not_found: 404,
637
+ precondition_failed: 412,
638
+ payload_too_large: 413,
639
+ unprocessable: 422,
640
+ content_policy_violation: 400,
641
+ refusal: 400,
642
+ context_length_exceeded: 400,
643
+ max_tokens_exceeded: 400,
644
+ token_limit_exceeded: 400,
645
+ string_too_long: 400,
646
+ invalid_request: 400,
647
+ invalid_prompt: 400,
648
+ invalid_image: 400,
649
+ image_too_large: 400,
650
+ image_too_small: 400,
651
+ unsupported_image_format: 400,
652
+ image_download_failed: 400
653
+ };
654
+ /** An error `code` as a detail string, whether the server sent a name or a numeric status. */
655
+ function getErrorCode(value) {
656
+ return typeof value === "number" ? String(value) : getString(value);
657
+ }
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.
@@ -27,14 +30,29 @@ export interface CreateIndeRunWebOptions {
27
30
  */
28
31
  hostServices?: CreateBrowserHostServicesOptions;
29
32
  /**
30
- * Explicit opt-in for calling the public OpenAI endpoint directly from this Web SDK factory.
33
+ * Explicit opt-in for calling a public Responses endpoint directly from this Web SDK factory —
34
+ * OpenAI's or OpenRouter's — with browser-held credentials.
31
35
  *
32
36
  * Browser apps should leave this unset and use a proxy endpoint with `openAI.auth = "none"` instead.
33
37
  * Direct calls can expose credentials to client-side code and are appropriate only for controlled
34
38
  * non-production environments.
35
39
  */
36
40
  allowDirectOpenAIEndpoint?: boolean;
41
+ /**
42
+ * Named model profiles requests can route within. Each maps a profile name to an
43
+ * ordered list of target ids, for example `{ smart: ["openai:gpt-5.2"] }`.
44
+ */
45
+ modelProfiles?: ModelProfiles;
37
46
  }
47
+ /**
48
+ * OpenAI options for {@link createIndeRunWeb}: the provider options with `models` in
49
+ * place of the single `model`. The first model is not special; every model becomes
50
+ * its own target.
51
+ */
52
+ export type OpenAIFactoryOptions = Omit<OpenAIProviderOptions, "model"> & {
53
+ /** OpenAI model ids to register, one target each. */
54
+ models: readonly string[];
55
+ };
38
56
  /**
39
57
  * Creates a Web SDK instance with the configured providers registered.
40
58
  *
@@ -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;;;;;;;OAOG;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,17 +38,19 @@ 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;
39
47
  const auth = openAI.auth ?? "authContextRef";
40
- const isDirectOpenAIEndpoint = isOpenAIResponsesEndpoint(endpointUrl);
41
- if (isDirectOpenAIEndpoint && auth !== "none" && !allowDirectOpenAIEndpoint) {
48
+ const isDirectPublicEndpoint = isPublicResponsesEndpoint(endpointUrl);
49
+ if (isDirectPublicEndpoint && auth !== "none" && !allowDirectOpenAIEndpoint) {
42
50
  throw new Error('createIndeRunWeb is proxy-first for browser safety. Configure openAI.endpointUrl to a server-side proxy with auth: "none", or set allowDirectOpenAIEndpoint: true for controlled direct OpenAI calls.');
43
51
  }
44
52
  }
45
- function isOpenAIResponsesEndpoint(endpointUrl) {
53
+ function isPublicResponsesEndpoint(endpointUrl) {
46
54
  let parsed;
47
55
  try {
48
56
  parsed = new URL(endpointUrl);
@@ -51,5 +59,14 @@ function isOpenAIResponsesEndpoint(endpointUrl) {
51
59
  return false;
52
60
  }
53
61
  const pathname = parsed.pathname.replace(/\/+$/u, "");
54
- return parsed.hostname.toLowerCase() === "api.openai.com" && pathname === "/v1/responses";
62
+ const endpoint = `${parsed.hostname.toLowerCase()}${pathname}`;
63
+ return PUBLIC_RESPONSES_ENDPOINTS.includes(endpoint);
55
64
  }
65
+ /**
66
+ * Hosted Responses endpoints whose credentials are developer-owned API keys, which the
67
+ * factory keeps out of browser code unless `allowDirectOpenAIEndpoint` says otherwise.
68
+ */
69
+ const PUBLIC_RESPONSES_ENDPOINTS = [
70
+ "api.openai.com/v1/responses",
71
+ "openrouter.ai/api/v1/responses"
72
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@independo/inderun-web",
3
- "version": "0.3.3",
3
+ "version": "0.4.0-rc.2",
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": {
@@ -47,6 +47,10 @@
47
47
  "types": "./dist/index.d.ts",
48
48
  "default": "./dist/index.js"
49
49
  },
50
+ "./provider": {
51
+ "types": "./dist/provider.d.ts",
52
+ "default": "./dist/provider.js"
53
+ },
50
54
  "./openai": {
51
55
  "types": "./dist/openai.d.ts",
52
56
  "default": "./dist/openai.js"
@@ -68,8 +72,8 @@
68
72
  "README.md"
69
73
  ],
70
74
  "dependencies": {
71
- "@independo/inderun-contracts": "^0.3.3",
72
- "@independo/inderun-route-core-wasm": "^0.3.3"
75
+ "@independo/inderun-contracts": "^0.4.0-rc.2",
76
+ "@independo/inderun-route-core-wasm": "^0.4.0-rc.2"
73
77
  },
74
78
  "peerDependencies": {
75
79
  "@huggingface/transformers": "^4.0.0"