@independo/inderun-web 0.4.0-rc.1 → 0.4.0
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 +30 -2
- package/dist/index.d.ts +35 -54
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -48
- package/dist/provider.d.ts +34 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +33 -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 +150 -58
- package/dist/web-sdk.d.ts +2 -1
- package/dist/web-sdk.d.ts.map +1 -1
- package/dist/web-sdk.js +13 -4
- package/package.json +7 -3
- package/src/index.ts +48 -120
- package/src/provider.ts +77 -0
- package/src/providers/openai/provider.ts +211 -68
- package/src/web-sdk.ts +16 -5
|
@@ -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();
|
|
@@ -102,11 +117,12 @@ type OpenAIResponseBody = {
|
|
|
102
117
|
incomplete_details?: {
|
|
103
118
|
reason?: unknown;
|
|
104
119
|
};
|
|
105
|
-
error?:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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;
|
|
110
126
|
};
|
|
111
127
|
|
|
112
128
|
type OpenAIErrorBody = {
|
|
@@ -304,7 +320,26 @@ export class OpenAIResponsesProvider implements ProviderAdapter {
|
|
|
304
320
|
}
|
|
305
321
|
|
|
306
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
|
+
|
|
307
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
|
+
}
|
|
308
343
|
if (outputText === undefined) {
|
|
309
344
|
throw createInternal("OpenAI Responses payload did not contain text output.", {
|
|
310
345
|
providerId: this.id,
|
|
@@ -356,7 +391,9 @@ export class OpenAIResponsesProvider implements ProviderAdapter {
|
|
|
356
391
|
* - `response.completed` and `response.incomplete` become the terminal done,
|
|
357
392
|
* with `finalText`, `usage`, and `finishReason` read off the embedded
|
|
358
393
|
* response object by the same helpers Mode 1 uses — it has the same shape;
|
|
359
|
-
* - `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);
|
|
360
397
|
* - every other event type is ignored, since the set is open and additive.
|
|
361
398
|
*
|
|
362
399
|
* @param request - Canonical IndeRun task request.
|
|
@@ -455,7 +492,12 @@ export class OpenAIResponsesProvider implements ProviderAdapter {
|
|
|
455
492
|
return;
|
|
456
493
|
}
|
|
457
494
|
|
|
458
|
-
if (
|
|
495
|
+
if (
|
|
496
|
+
type === "response.failed" ||
|
|
497
|
+
type === "response.error" ||
|
|
498
|
+
type === "error" ||
|
|
499
|
+
(type === undefined && isRecord(parsed.error))
|
|
500
|
+
) {
|
|
459
501
|
yield { kind: "error", error: this.mapStreamError(parsed, context) };
|
|
460
502
|
return;
|
|
461
503
|
}
|
|
@@ -467,30 +509,79 @@ export class OpenAIResponsesProvider implements ProviderAdapter {
|
|
|
467
509
|
}
|
|
468
510
|
|
|
469
511
|
/**
|
|
470
|
-
* Maps a `response.failed` / `error` stream event onto the
|
|
471
|
-
* These arrive over an HTTP 200 body, so there is no status
|
|
472
|
-
*
|
|
473
|
-
* `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.
|
|
474
515
|
*
|
|
475
516
|
* Two payload shapes exist and both are accepted: the standalone `error`
|
|
476
517
|
* event carries `code`/`message` at its root, while `response.failed` nests
|
|
477
|
-
* them under `response.error
|
|
478
|
-
* 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).
|
|
479
522
|
*/
|
|
480
523
|
private mapStreamError(event: OpenAIStreamEvent, context: ProviderStreamContext): unknown {
|
|
481
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 {
|
|
482
555
|
const message =
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
+
};
|
|
486
564
|
const params = { providerId: this.id, runId: context.runId, details };
|
|
487
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
|
+
|
|
488
575
|
switch (code) {
|
|
489
576
|
case "rate_limit_exceeded":
|
|
490
577
|
return createRateLimited(message, { ...params, retryable: true });
|
|
491
578
|
case "invalid_api_key":
|
|
492
579
|
case "authentication_error":
|
|
493
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);
|
|
494
585
|
default:
|
|
495
586
|
return createUnavailable(message, { ...params, retryable: true });
|
|
496
587
|
}
|
|
@@ -573,12 +664,9 @@ export class OpenAIResponsesProvider implements ProviderAdapter {
|
|
|
573
664
|
}
|
|
574
665
|
|
|
575
666
|
/**
|
|
576
|
-
* Maps an OpenAI HTTP error response onto the IndeRun error taxonomy
|
|
577
|
-
*
|
|
578
|
-
*
|
|
579
|
-
* - `408` / `504` → `Timeout`
|
|
580
|
-
* - `409` / `5xx` → `Unavailable`
|
|
581
|
-
* - 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.
|
|
582
670
|
*/
|
|
583
671
|
private mapHttpError(
|
|
584
672
|
status: number,
|
|
@@ -589,63 +677,70 @@ export class OpenAIResponsesProvider implements ProviderAdapter {
|
|
|
589
677
|
): Error {
|
|
590
678
|
const error = parseJson<OpenAIErrorBody>(body);
|
|
591
679
|
const message = getOpenAIErrorMessage(error, status, statusText);
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
+
}
|
|
597
689
|
};
|
|
598
690
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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;
|
|
605
719
|
}
|
|
606
720
|
|
|
607
|
-
if (status ===
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
providerId: this.id,
|
|
611
|
-
runId: context.runId,
|
|
612
|
-
retryable: true,
|
|
613
|
-
details
|
|
614
|
-
};
|
|
721
|
+
if (status === 401 || status === 403) {
|
|
722
|
+
return createAuthError(message, params);
|
|
723
|
+
}
|
|
615
724
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
}
|
|
725
|
+
if (status === 402) {
|
|
726
|
+
return retryAfterMs === undefined
|
|
727
|
+
? createUnavailable(message, { ...params, retryable: false })
|
|
728
|
+
: createRateLimited(message, retry);
|
|
729
|
+
}
|
|
622
730
|
|
|
623
|
-
|
|
731
|
+
if (status === 429) {
|
|
732
|
+
return createRateLimited(message, retry);
|
|
624
733
|
}
|
|
625
734
|
|
|
626
735
|
if (status === 408 || status === 504) {
|
|
627
|
-
return createTimeout(message,
|
|
628
|
-
providerId: this.id,
|
|
629
|
-
runId: context.runId,
|
|
630
|
-
retryable: true,
|
|
631
|
-
details
|
|
632
|
-
});
|
|
736
|
+
return createTimeout(message, retry);
|
|
633
737
|
}
|
|
634
738
|
|
|
635
739
|
if (status === 409 || status >= 500) {
|
|
636
|
-
return createUnavailable(message,
|
|
637
|
-
providerId: this.id,
|
|
638
|
-
runId: context.runId,
|
|
639
|
-
retryable: true,
|
|
640
|
-
details
|
|
641
|
-
});
|
|
740
|
+
return createUnavailable(message, retry);
|
|
642
741
|
}
|
|
643
742
|
|
|
644
|
-
return createInternal(message,
|
|
645
|
-
providerId: this.id,
|
|
646
|
-
runId: context.runId,
|
|
647
|
-
details
|
|
648
|
-
});
|
|
743
|
+
return createInternal(message, params);
|
|
649
744
|
}
|
|
650
745
|
}
|
|
651
746
|
|
|
@@ -784,3 +879,51 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
784
879
|
function getString(value: unknown): string | undefined {
|
|
785
880
|
return typeof value === "string" ? value : undefined;
|
|
786
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
|
@@ -42,7 +42,8 @@ export interface CreateIndeRunWebOptions {
|
|
|
42
42
|
*/
|
|
43
43
|
hostServices?: CreateBrowserHostServicesOptions;
|
|
44
44
|
/**
|
|
45
|
-
* 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.
|
|
46
47
|
*
|
|
47
48
|
* Browser apps should leave this unset and use a proxy endpoint with `openAI.auth = "none"` instead.
|
|
48
49
|
* Direct calls can expose credentials to client-side code and are appropriate only for controlled
|
|
@@ -118,16 +119,16 @@ function assertSafeOpenAIEndpoint(
|
|
|
118
119
|
): void {
|
|
119
120
|
const endpointUrl = openAI.endpointUrl ?? DEFAULT_OPENAI_RESPONSES_ENDPOINT;
|
|
120
121
|
const auth = openAI.auth ?? "authContextRef";
|
|
121
|
-
const
|
|
122
|
+
const isDirectPublicEndpoint = isPublicResponsesEndpoint(endpointUrl);
|
|
122
123
|
|
|
123
|
-
if (
|
|
124
|
+
if (isDirectPublicEndpoint && auth !== "none" && !allowDirectOpenAIEndpoint) {
|
|
124
125
|
throw new Error(
|
|
125
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.'
|
|
126
127
|
);
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
function
|
|
131
|
+
function isPublicResponsesEndpoint(endpointUrl: string): boolean {
|
|
131
132
|
let parsed: URL;
|
|
132
133
|
try {
|
|
133
134
|
parsed = new URL(endpointUrl);
|
|
@@ -136,5 +137,15 @@ function isOpenAIResponsesEndpoint(endpointUrl: string): boolean {
|
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
const pathname = parsed.pathname.replace(/\/+$/u, "");
|
|
139
|
-
|
|
140
|
+
const endpoint = `${parsed.hostname.toLowerCase()}${pathname}`;
|
|
141
|
+
return PUBLIC_RESPONSES_ENDPOINTS.includes(endpoint);
|
|
140
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
|
+
];
|