@independo/inderun-web 0.3.3 → 0.4.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -9
- package/dist/core/engine.d.ts +26 -2
- package/dist/core/engine.d.ts.map +1 -1
- package/dist/core/engine.js +78 -34
- package/dist/core/errors.d.ts +13 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +32 -0
- package/dist/core/model-profiles.d.ts +34 -0
- package/dist/core/model-profiles.d.ts.map +1 -0
- package/dist/core/model-profiles.js +49 -0
- package/dist/core/provenance.d.ts +32 -0
- package/dist/core/provenance.d.ts.map +1 -0
- package/dist/core/provenance.js +71 -0
- package/dist/core/provider.d.ts +13 -0
- package/dist/core/provider.d.ts.map +1 -1
- package/dist/core/route-planner.d.ts +1 -1
- package/dist/core/route-planner.d.ts.map +1 -1
- package/dist/core/route-planner.js +2 -1
- package/dist/core/router.d.ts +4 -3
- package/dist/core/router.d.ts.map +1 -1
- package/dist/core/router.js +4 -3
- package/dist/facade/text.d.ts +123 -0
- package/dist/facade/text.d.ts.map +1 -0
- package/dist/facade/text.js +234 -0
- package/dist/index.d.ts +36 -45
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -38
- package/dist/provider.d.ts +34 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +33 -0
- package/dist/providers/onnx/provider.d.ts.map +1 -1
- package/dist/providers/onnx/provider.js +1 -0
- package/dist/providers/openai/provider.d.ts +41 -11
- package/dist/providers/openai/provider.d.ts.map +1 -1
- package/dist/providers/openai/provider.js +168 -58
- package/dist/web-sdk.d.ts +21 -3
- package/dist/web-sdk.d.ts.map +1 -1
- package/dist/web-sdk.js +23 -6
- package/package.json +7 -3
- package/src/core/engine.ts +114 -35
- package/src/core/errors.ts +35 -1
- package/src/core/model-profiles.ts +70 -0
- package/src/core/provenance.ts +90 -0
- package/src/core/provider.ts +13 -0
- package/src/core/route-planner.ts +3 -1
- package/src/core/router.ts +12 -3
- package/src/facade/text.ts +349 -0
- package/src/index.ts +49 -95
- package/src/provider.ts +77 -0
- package/src/providers/onnx/provider.ts +1 -0
- package/src/providers/openai/provider.ts +232 -68
- package/src/web-sdk.ts +47 -10
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
createInternal,
|
|
5
5
|
createRateLimited,
|
|
6
6
|
createTimeout,
|
|
7
|
-
createUnavailable
|
|
7
|
+
createUnavailable,
|
|
8
|
+
type IndeRunExceptionParams
|
|
8
9
|
} from "../../core/errors.js";
|
|
9
10
|
import type { HostServices, HttpRequest } from "../../core/host.js";
|
|
10
11
|
import type {
|
|
@@ -26,12 +27,26 @@ interface OpenAIStreamEvent {
|
|
|
26
27
|
type?: string;
|
|
27
28
|
delta?: string;
|
|
28
29
|
response?: OpenAIResponseBody;
|
|
29
|
-
error?:
|
|
30
|
+
error?: OpenAIReportedError;
|
|
30
31
|
/** Root-level failure fields, as carried by the standalone `error` event. */
|
|
31
32
|
message?: string;
|
|
32
|
-
code?:
|
|
33
|
+
code?: unknown;
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* A failure reported inside a response body or stream event. `code` is a string
|
|
38
|
+
* on OpenAI and the numeric HTTP status on OpenRouter, so it is read as either.
|
|
39
|
+
*/
|
|
40
|
+
interface OpenAIReportedError {
|
|
41
|
+
message?: string | undefined;
|
|
42
|
+
type?: string | undefined;
|
|
43
|
+
code?: unknown;
|
|
44
|
+
/** OpenRouter's typed classification on an untyped (chat-completions-shaped) error event. */
|
|
45
|
+
metadata?: { error_type?: unknown } | undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type ErrorParams = Omit<IndeRunExceptionParams, "errorClass" | "message">;
|
|
49
|
+
|
|
35
50
|
/** Buffers a body stream to text, for error responses that are not event streams. */
|
|
36
51
|
async function drainToText(body: AsyncIterable<Uint8Array>): Promise<string> {
|
|
37
52
|
const decoder = new TextDecoder();
|
|
@@ -90,6 +105,7 @@ export interface OpenAIProviderOptions {
|
|
|
90
105
|
}
|
|
91
106
|
|
|
92
107
|
type OpenAIResponseBody = {
|
|
108
|
+
model?: unknown;
|
|
93
109
|
output_text?: unknown;
|
|
94
110
|
output?: unknown;
|
|
95
111
|
status?: unknown;
|
|
@@ -101,11 +117,12 @@ type OpenAIResponseBody = {
|
|
|
101
117
|
incomplete_details?: {
|
|
102
118
|
reason?: unknown;
|
|
103
119
|
};
|
|
104
|
-
error?:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
120
|
+
error?: OpenAIReportedError;
|
|
121
|
+
/**
|
|
122
|
+
* OpenRouter's typed classification of a failed response. It sits beside
|
|
123
|
+
* `error`, whose native `code` collapses many causes into `server_error`.
|
|
124
|
+
*/
|
|
125
|
+
error_type?: unknown;
|
|
109
126
|
};
|
|
110
127
|
|
|
111
128
|
type OpenAIErrorBody = {
|
|
@@ -146,6 +163,7 @@ export class OpenAIResponsesProvider implements ProviderAdapter {
|
|
|
146
163
|
return {
|
|
147
164
|
id: this.id,
|
|
148
165
|
type: "cloud",
|
|
166
|
+
model: this.options.model,
|
|
149
167
|
transport: "http",
|
|
150
168
|
streamingStyle: "tokens",
|
|
151
169
|
supports: {
|
|
@@ -302,7 +320,26 @@ export class OpenAIResponsesProvider implements ProviderAdapter {
|
|
|
302
320
|
}
|
|
303
321
|
|
|
304
322
|
const responseBody = parseJson<OpenAIResponseBody>(response.body);
|
|
323
|
+
// A lenient server (OpenRouter among them) can answer 2xx for a run that
|
|
324
|
+
// failed upstream; that is a failure to classify, not an empty answer.
|
|
325
|
+
if (responseBody.status === "failed" || responseBody.status === "cancelled") {
|
|
326
|
+
throw this.mapReportedError(
|
|
327
|
+
responseBody.error,
|
|
328
|
+
`OpenAI Responses run ended with status '${responseBody.status}'.`,
|
|
329
|
+
context,
|
|
330
|
+
responseBody.error_type
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
305
334
|
const outputText = extractOutputText(responseBody);
|
|
335
|
+
if (outputText === undefined && isRecord(responseBody.error)) {
|
|
336
|
+
throw this.mapReportedError(
|
|
337
|
+
responseBody.error,
|
|
338
|
+
"OpenAI Responses payload reported an error.",
|
|
339
|
+
context,
|
|
340
|
+
responseBody.error_type
|
|
341
|
+
);
|
|
342
|
+
}
|
|
306
343
|
if (outputText === undefined) {
|
|
307
344
|
throw createInternal("OpenAI Responses payload did not contain text output.", {
|
|
308
345
|
providerId: this.id,
|
|
@@ -324,6 +361,11 @@ export class OpenAIResponsesProvider implements ProviderAdapter {
|
|
|
324
361
|
}
|
|
325
362
|
};
|
|
326
363
|
|
|
364
|
+
const reportedModel = getReportedModel(responseBody);
|
|
365
|
+
if (reportedModel !== undefined) {
|
|
366
|
+
result.telemetry.reportedModel = reportedModel;
|
|
367
|
+
}
|
|
368
|
+
|
|
327
369
|
const usage = getUsage(responseBody);
|
|
328
370
|
if (usage) {
|
|
329
371
|
result.usage = usage;
|
|
@@ -349,7 +391,9 @@ export class OpenAIResponsesProvider implements ProviderAdapter {
|
|
|
349
391
|
* - `response.completed` and `response.incomplete` become the terminal done,
|
|
350
392
|
* with `finalText`, `usage`, and `finishReason` read off the embedded
|
|
351
393
|
* response object by the same helpers Mode 1 uses — it has the same shape;
|
|
352
|
-
* - `response.failed` and `error` become a terminal error
|
|
394
|
+
* - `response.failed`, `response.error` and `error` become a terminal error,
|
|
395
|
+
* as does an untyped event carrying an `error` object (OpenRouter's
|
|
396
|
+
* documented mid-stream failure shape);
|
|
353
397
|
* - every other event type is ignored, since the set is open and additive.
|
|
354
398
|
*
|
|
355
399
|
* @param request - Canonical IndeRun task request.
|
|
@@ -440,11 +484,20 @@ export class OpenAIResponsesProvider implements ProviderAdapter {
|
|
|
440
484
|
if (usage) {
|
|
441
485
|
done.usage = usage;
|
|
442
486
|
}
|
|
487
|
+
const reportedModel = getReportedModel(body);
|
|
488
|
+
if (reportedModel !== undefined) {
|
|
489
|
+
done.reportedModel = reportedModel;
|
|
490
|
+
}
|
|
443
491
|
yield done;
|
|
444
492
|
return;
|
|
445
493
|
}
|
|
446
494
|
|
|
447
|
-
if (
|
|
495
|
+
if (
|
|
496
|
+
type === "response.failed" ||
|
|
497
|
+
type === "response.error" ||
|
|
498
|
+
type === "error" ||
|
|
499
|
+
(type === undefined && isRecord(parsed.error))
|
|
500
|
+
) {
|
|
448
501
|
yield { kind: "error", error: this.mapStreamError(parsed, context) };
|
|
449
502
|
return;
|
|
450
503
|
}
|
|
@@ -456,30 +509,79 @@ export class OpenAIResponsesProvider implements ProviderAdapter {
|
|
|
456
509
|
}
|
|
457
510
|
|
|
458
511
|
/**
|
|
459
|
-
* Maps a `response.failed` / `error` stream event onto the
|
|
460
|
-
* These arrive over an HTTP 200 body, so there is no status
|
|
461
|
-
*
|
|
462
|
-
* `code` values it uses in unary error bodies.
|
|
512
|
+
* Maps a `response.failed` / `response.error` / `error` stream event onto the
|
|
513
|
+
* error taxonomy. These arrive over an HTTP 200 body, so there is no status
|
|
514
|
+
* code to classify from; the event's own `code` is classified instead.
|
|
463
515
|
*
|
|
464
516
|
* Two payload shapes exist and both are accepted: the standalone `error`
|
|
465
517
|
* event carries `code`/`message` at its root, while `response.failed` nests
|
|
466
|
-
* them under `response.error
|
|
467
|
-
* an error type, so it is never
|
|
518
|
+
* them under `response.error` and some servers nest them under `error`. The
|
|
519
|
+
* root `type` is the event name rather than an error type, so it is never
|
|
520
|
+
* read as one. OpenRouter's typed classification comes from the embedded
|
|
521
|
+
* response (`response.failed`) or the error's metadata (untyped events).
|
|
468
522
|
*/
|
|
469
523
|
private mapStreamError(event: OpenAIStreamEvent, context: ProviderStreamContext): unknown {
|
|
470
524
|
const nested = event.error ?? event.response?.error;
|
|
525
|
+
return this.mapReportedError(
|
|
526
|
+
{
|
|
527
|
+
message: nested?.message ?? event.message,
|
|
528
|
+
type: nested?.type,
|
|
529
|
+
code: nested?.code ?? event.code
|
|
530
|
+
},
|
|
531
|
+
"OpenAI Responses stream reported a failure.",
|
|
532
|
+
context,
|
|
533
|
+
event.response?.error_type ?? nested?.metadata?.error_type
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Classifies a failure reported in a body rather than by the HTTP status: a
|
|
539
|
+
* failed stream event or a 2xx run body whose status is `failed`. The most
|
|
540
|
+
* specific signal wins:
|
|
541
|
+
* 1. OpenRouter's typed `error_type`, classified by the HTTP status it
|
|
542
|
+
* documents for that type (see `openRouterErrorTypeStatus`);
|
|
543
|
+
* 2. an integer `code`, which is an HTTP status (OpenRouter's convention) and
|
|
544
|
+
* is classified exactly as `mapHttpError` classifies that status;
|
|
545
|
+
* 3. a string code, by OpenAI's names.
|
|
546
|
+
* Anything unrecognized is a transient `Unavailable`, since the server did
|
|
547
|
+
* accept the request.
|
|
548
|
+
*/
|
|
549
|
+
private mapReportedError(
|
|
550
|
+
reported: OpenAIReportedError | undefined,
|
|
551
|
+
fallbackMessage: string,
|
|
552
|
+
context: RunContext,
|
|
553
|
+
errorType?: unknown
|
|
554
|
+
): Error {
|
|
471
555
|
const message =
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
556
|
+
typeof reported?.message === "string" && reported.message.length > 0
|
|
557
|
+
? reported.message
|
|
558
|
+
: fallbackMessage;
|
|
559
|
+
const typedStatus = openRouterErrorTypeStatus(errorType);
|
|
560
|
+
const details = {
|
|
561
|
+
errorType: typedStatus !== undefined ? (errorType as string) : reported?.type,
|
|
562
|
+
errorCode: getErrorCode(reported?.code)
|
|
563
|
+
};
|
|
475
564
|
const params = { providerId: this.id, runId: context.runId, details };
|
|
476
565
|
|
|
566
|
+
if (typedStatus !== undefined) {
|
|
567
|
+
return this.errorForStatus(typedStatus, message, params);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const code = reported?.code;
|
|
571
|
+
if (typeof code === "number" && Number.isInteger(code)) {
|
|
572
|
+
return this.errorForStatus(code, message, params);
|
|
573
|
+
}
|
|
574
|
+
|
|
477
575
|
switch (code) {
|
|
478
576
|
case "rate_limit_exceeded":
|
|
479
577
|
return createRateLimited(message, { ...params, retryable: true });
|
|
480
578
|
case "invalid_api_key":
|
|
481
579
|
case "authentication_error":
|
|
482
580
|
return createAuthError(message, params);
|
|
581
|
+
// The request itself was rejected; repeating it cannot succeed.
|
|
582
|
+
case "invalid_prompt":
|
|
583
|
+
case "image_content_policy_violation":
|
|
584
|
+
return createInternal(message, params);
|
|
483
585
|
default:
|
|
484
586
|
return createUnavailable(message, { ...params, retryable: true });
|
|
485
587
|
}
|
|
@@ -562,12 +664,9 @@ export class OpenAIResponsesProvider implements ProviderAdapter {
|
|
|
562
664
|
}
|
|
563
665
|
|
|
564
666
|
/**
|
|
565
|
-
* Maps an OpenAI HTTP error response onto the IndeRun error taxonomy
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
* - `408` / `504` → `Timeout`
|
|
569
|
-
* - `409` / `5xx` → `Unavailable`
|
|
570
|
-
* - any other non-2xx status → `Internal`
|
|
667
|
+
* Maps an OpenAI HTTP error response onto the IndeRun error taxonomy by its
|
|
668
|
+
* status (see `errorForStatus`), honoring `Retry-After` on whichever status
|
|
669
|
+
* carries it.
|
|
571
670
|
*/
|
|
572
671
|
private mapHttpError(
|
|
573
672
|
status: number,
|
|
@@ -578,63 +677,70 @@ export class OpenAIResponsesProvider implements ProviderAdapter {
|
|
|
578
677
|
): Error {
|
|
579
678
|
const error = parseJson<OpenAIErrorBody>(body);
|
|
580
679
|
const message = getOpenAIErrorMessage(error, status, statusText);
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
680
|
+
const params: ErrorParams = {
|
|
681
|
+
providerId: this.id,
|
|
682
|
+
runId: context.runId,
|
|
683
|
+
details: {
|
|
684
|
+
status,
|
|
685
|
+
statusText,
|
|
686
|
+
errorType: getString(error.error?.type),
|
|
687
|
+
errorCode: getErrorCode(error.error?.code)
|
|
688
|
+
}
|
|
586
689
|
};
|
|
587
690
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
691
|
+
return this.errorForStatus(status, message, params, parseRetryAfterMs(headers));
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* The status classification shared by HTTP errors and numeric in-body codes:
|
|
696
|
+
* - `401` / `403` → `AuthError`
|
|
697
|
+
* - `402` → `Unavailable`, not retryable: the account is out of credits, so
|
|
698
|
+
* this target cannot serve until someone tops it up. A 402 that carries
|
|
699
|
+
* `Retry-After` is the exception — OpenRouter documents it for its
|
|
700
|
+
* in-flight budget limit, a wait-and-retry case — and is `RateLimited`. Fallback is unaffected
|
|
701
|
+
* either way: the engine moves on to the next target whatever the class.
|
|
702
|
+
* - `429` → `RateLimited`
|
|
703
|
+
* - `408` / `504` → `Timeout`
|
|
704
|
+
* - `409` / `5xx` → `Unavailable`
|
|
705
|
+
* - any other status → `Internal`
|
|
706
|
+
*
|
|
707
|
+
* `retryAfterMs` travels on every retryable class; it is absent for numeric
|
|
708
|
+
* in-body codes, which have no header to read it from.
|
|
709
|
+
*/
|
|
710
|
+
private errorForStatus(
|
|
711
|
+
status: number,
|
|
712
|
+
message: string,
|
|
713
|
+
params: ErrorParams,
|
|
714
|
+
retryAfterMs?: number
|
|
715
|
+
): Error {
|
|
716
|
+
const retry: ErrorParams = { ...params, retryable: true };
|
|
717
|
+
if (retryAfterMs !== undefined) {
|
|
718
|
+
retry.retryAfterMs = retryAfterMs;
|
|
594
719
|
}
|
|
595
720
|
|
|
596
|
-
if (status ===
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
providerId: this.id,
|
|
600
|
-
runId: context.runId,
|
|
601
|
-
retryable: true,
|
|
602
|
-
details
|
|
603
|
-
};
|
|
721
|
+
if (status === 401 || status === 403) {
|
|
722
|
+
return createAuthError(message, params);
|
|
723
|
+
}
|
|
604
724
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
725
|
+
if (status === 402) {
|
|
726
|
+
return retryAfterMs === undefined
|
|
727
|
+
? createUnavailable(message, { ...params, retryable: false })
|
|
728
|
+
: createRateLimited(message, retry);
|
|
729
|
+
}
|
|
611
730
|
|
|
612
|
-
|
|
731
|
+
if (status === 429) {
|
|
732
|
+
return createRateLimited(message, retry);
|
|
613
733
|
}
|
|
614
734
|
|
|
615
735
|
if (status === 408 || status === 504) {
|
|
616
|
-
return createTimeout(message,
|
|
617
|
-
providerId: this.id,
|
|
618
|
-
runId: context.runId,
|
|
619
|
-
retryable: true,
|
|
620
|
-
details
|
|
621
|
-
});
|
|
736
|
+
return createTimeout(message, retry);
|
|
622
737
|
}
|
|
623
738
|
|
|
624
739
|
if (status === 409 || status >= 500) {
|
|
625
|
-
return createUnavailable(message,
|
|
626
|
-
providerId: this.id,
|
|
627
|
-
runId: context.runId,
|
|
628
|
-
retryable: true,
|
|
629
|
-
details
|
|
630
|
-
});
|
|
740
|
+
return createUnavailable(message, retry);
|
|
631
741
|
}
|
|
632
742
|
|
|
633
|
-
return createInternal(message,
|
|
634
|
-
providerId: this.id,
|
|
635
|
-
runId: context.runId,
|
|
636
|
-
details
|
|
637
|
-
});
|
|
743
|
+
return createInternal(message, params);
|
|
638
744
|
}
|
|
639
745
|
}
|
|
640
746
|
|
|
@@ -686,6 +792,16 @@ function getFinishReason(response: OpenAIResponseBody): TaskResult["finishReason
|
|
|
686
792
|
return "stop";
|
|
687
793
|
}
|
|
688
794
|
|
|
795
|
+
/**
|
|
796
|
+
* The model id the Responses API echoes on the response object. It names the model that
|
|
797
|
+
* served the request, which can be a dated snapshot of the configured alias.
|
|
798
|
+
*/
|
|
799
|
+
function getReportedModel(responseBody: OpenAIResponseBody): string | undefined {
|
|
800
|
+
return typeof responseBody.model === "string" && responseBody.model.length > 0
|
|
801
|
+
? responseBody.model
|
|
802
|
+
: undefined;
|
|
803
|
+
}
|
|
804
|
+
|
|
689
805
|
function getUsage(response: OpenAIResponseBody): TaskResult["usage"] | undefined {
|
|
690
806
|
const usage: TaskResult["usage"] = {};
|
|
691
807
|
|
|
@@ -763,3 +879,51 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
763
879
|
function getString(value: unknown): string | undefined {
|
|
764
880
|
return typeof value === "string" ? value : undefined;
|
|
765
881
|
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* The HTTP status OpenRouter documents for each typed `error_type`, so a typed
|
|
885
|
+
* failure is classified exactly like the equivalent HTTP error. Refusals and
|
|
886
|
+
* content-policy violations are documented as 403 but are not credential
|
|
887
|
+
* failures, so they classify as a rejected request (400). Unknown types return
|
|
888
|
+
* `undefined` and the native `code` decides instead.
|
|
889
|
+
*/
|
|
890
|
+
function openRouterErrorTypeStatus(errorType: unknown): number | undefined {
|
|
891
|
+
return typeof errorType === "string" && Object.hasOwn(OPENROUTER_ERROR_TYPE_STATUS, errorType)
|
|
892
|
+
? OPENROUTER_ERROR_TYPE_STATUS[errorType]
|
|
893
|
+
: undefined;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const OPENROUTER_ERROR_TYPE_STATUS: Readonly<Record<string, number>> = {
|
|
897
|
+
authentication: 401,
|
|
898
|
+
payment_required: 402,
|
|
899
|
+
permission_denied: 403,
|
|
900
|
+
rate_limit_exceeded: 429,
|
|
901
|
+
timeout: 504,
|
|
902
|
+
provider_unavailable: 502,
|
|
903
|
+
provider_overloaded: 503,
|
|
904
|
+
server: 500,
|
|
905
|
+
unmapped: 500,
|
|
906
|
+
not_found: 404,
|
|
907
|
+
image_not_found: 404,
|
|
908
|
+
precondition_failed: 412,
|
|
909
|
+
payload_too_large: 413,
|
|
910
|
+
unprocessable: 422,
|
|
911
|
+
content_policy_violation: 400,
|
|
912
|
+
refusal: 400,
|
|
913
|
+
context_length_exceeded: 400,
|
|
914
|
+
max_tokens_exceeded: 400,
|
|
915
|
+
token_limit_exceeded: 400,
|
|
916
|
+
string_too_long: 400,
|
|
917
|
+
invalid_request: 400,
|
|
918
|
+
invalid_prompt: 400,
|
|
919
|
+
invalid_image: 400,
|
|
920
|
+
image_too_large: 400,
|
|
921
|
+
image_too_small: 400,
|
|
922
|
+
unsupported_image_format: 400,
|
|
923
|
+
image_download_failed: 400
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
/** An error `code` as a detail string, whether the server sent a name or a numeric status. */
|
|
927
|
+
function getErrorCode(value: unknown): string | undefined {
|
|
928
|
+
return typeof value === "number" ? String(value) : getString(value);
|
|
929
|
+
}
|
package/src/web-sdk.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type CreateBrowserHostServicesOptions
|
|
4
4
|
} from "./core/browser-host.js";
|
|
5
5
|
import { IndeRun } from "./core/engine.js";
|
|
6
|
+
import type { ModelProfiles } from "./core/model-profiles.js";
|
|
6
7
|
import { OnnxRuntimeWebProvider, type OnnxProviderOptions } from "./providers/onnx/provider.js";
|
|
7
8
|
import {
|
|
8
9
|
DEFAULT_OPENAI_RESPONSES_ENDPOINT,
|
|
@@ -20,9 +21,11 @@ import {
|
|
|
20
21
|
*/
|
|
21
22
|
export interface CreateIndeRunWebOptions {
|
|
22
23
|
/**
|
|
23
|
-
* OpenAI Responses provider configuration registered by the factory.
|
|
24
|
+
* OpenAI Responses provider configuration registered by the factory. One target is
|
|
25
|
+
* registered per entry in `models`, with id `<id>:<model>` (`id` defaults to
|
|
26
|
+
* `"openai"`), so model profiles can list each model separately.
|
|
24
27
|
*/
|
|
25
|
-
openAI?:
|
|
28
|
+
openAI?: OpenAIFactoryOptions;
|
|
26
29
|
/**
|
|
27
30
|
* Web ONNX Runtime provider configuration registered by the factory, for developer-supplied
|
|
28
31
|
* local models. Registering it is what makes `constraints.privacy = "local_required"` routable.
|
|
@@ -39,15 +42,31 @@ export interface CreateIndeRunWebOptions {
|
|
|
39
42
|
*/
|
|
40
43
|
hostServices?: CreateBrowserHostServicesOptions;
|
|
41
44
|
/**
|
|
42
|
-
* Explicit opt-in for calling
|
|
45
|
+
* Explicit opt-in for calling a public Responses endpoint directly from this Web SDK factory —
|
|
46
|
+
* OpenAI's or OpenRouter's — with browser-held credentials.
|
|
43
47
|
*
|
|
44
48
|
* Browser apps should leave this unset and use a proxy endpoint with `openAI.auth = "none"` instead.
|
|
45
49
|
* Direct calls can expose credentials to client-side code and are appropriate only for controlled
|
|
46
50
|
* non-production environments.
|
|
47
51
|
*/
|
|
48
52
|
allowDirectOpenAIEndpoint?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Named model profiles requests can route within. Each maps a profile name to an
|
|
55
|
+
* ordered list of target ids, for example `{ smart: ["openai:gpt-5.2"] }`.
|
|
56
|
+
*/
|
|
57
|
+
modelProfiles?: ModelProfiles;
|
|
49
58
|
}
|
|
50
59
|
|
|
60
|
+
/**
|
|
61
|
+
* OpenAI options for {@link createIndeRunWeb}: the provider options with `models` in
|
|
62
|
+
* place of the single `model`. The first model is not special; every model becomes
|
|
63
|
+
* its own target.
|
|
64
|
+
*/
|
|
65
|
+
export type OpenAIFactoryOptions = Omit<OpenAIProviderOptions, "model"> & {
|
|
66
|
+
/** OpenAI model ids to register, one target each. */
|
|
67
|
+
models: readonly string[];
|
|
68
|
+
};
|
|
69
|
+
|
|
51
70
|
/**
|
|
52
71
|
* Creates a Web SDK instance with the configured providers registered.
|
|
53
72
|
*
|
|
@@ -72,7 +91,13 @@ export function createIndeRunWeb(options: CreateIndeRunWebOptions): IndeRun {
|
|
|
72
91
|
|
|
73
92
|
if (options.openAI) {
|
|
74
93
|
assertSafeOpenAIEndpoint(options.openAI, options.allowDirectOpenAIEndpoint);
|
|
75
|
-
|
|
94
|
+
const { models, id = "openai", ...shared } = options.openAI;
|
|
95
|
+
if (models.length === 0) {
|
|
96
|
+
throw new Error("createIndeRunWeb requires openAI.models to list at least one model.");
|
|
97
|
+
}
|
|
98
|
+
for (const model of models) {
|
|
99
|
+
registry.register(new OpenAIResponsesProvider({ ...shared, id: `${id}:${model}`, model }));
|
|
100
|
+
}
|
|
76
101
|
}
|
|
77
102
|
|
|
78
103
|
if (options.onnx) {
|
|
@@ -83,25 +108,27 @@ export function createIndeRunWeb(options: CreateIndeRunWebOptions): IndeRun {
|
|
|
83
108
|
registry.register(new SystemModelWebProvider(options.systemModel));
|
|
84
109
|
}
|
|
85
110
|
|
|
86
|
-
return new IndeRun(registry, createBrowserHostServices(options.hostServices)
|
|
111
|
+
return new IndeRun(registry, createBrowserHostServices(options.hostServices), {
|
|
112
|
+
...(options.modelProfiles ? { modelProfiles: options.modelProfiles } : {})
|
|
113
|
+
});
|
|
87
114
|
}
|
|
88
115
|
|
|
89
116
|
function assertSafeOpenAIEndpoint(
|
|
90
|
-
openAI:
|
|
117
|
+
openAI: OpenAIFactoryOptions,
|
|
91
118
|
allowDirectOpenAIEndpoint?: boolean
|
|
92
119
|
): void {
|
|
93
120
|
const endpointUrl = openAI.endpointUrl ?? DEFAULT_OPENAI_RESPONSES_ENDPOINT;
|
|
94
121
|
const auth = openAI.auth ?? "authContextRef";
|
|
95
|
-
const
|
|
122
|
+
const isDirectPublicEndpoint = isPublicResponsesEndpoint(endpointUrl);
|
|
96
123
|
|
|
97
|
-
if (
|
|
124
|
+
if (isDirectPublicEndpoint && auth !== "none" && !allowDirectOpenAIEndpoint) {
|
|
98
125
|
throw new Error(
|
|
99
126
|
'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.'
|
|
100
127
|
);
|
|
101
128
|
}
|
|
102
129
|
}
|
|
103
130
|
|
|
104
|
-
function
|
|
131
|
+
function isPublicResponsesEndpoint(endpointUrl: string): boolean {
|
|
105
132
|
let parsed: URL;
|
|
106
133
|
try {
|
|
107
134
|
parsed = new URL(endpointUrl);
|
|
@@ -110,5 +137,15 @@ function isOpenAIResponsesEndpoint(endpointUrl: string): boolean {
|
|
|
110
137
|
}
|
|
111
138
|
|
|
112
139
|
const pathname = parsed.pathname.replace(/\/+$/u, "");
|
|
113
|
-
|
|
140
|
+
const endpoint = `${parsed.hostname.toLowerCase()}${pathname}`;
|
|
141
|
+
return PUBLIC_RESPONSES_ENDPOINTS.includes(endpoint);
|
|
114
142
|
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Hosted Responses endpoints whose credentials are developer-owned API keys, which the
|
|
146
|
+
* factory keeps out of browser code unless `allowDirectOpenAIEndpoint` says otherwise.
|
|
147
|
+
*/
|
|
148
|
+
const PUBLIC_RESPONSES_ENDPOINTS = [
|
|
149
|
+
"api.openai.com/v1/responses",
|
|
150
|
+
"openrouter.ai/api/v1/responses"
|
|
151
|
+
];
|