@plurnk/plurnk-providers 1.3.12 → 1.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/.env.defaults +39 -22
- package/README.md +65 -4
- package/SPEC.md +222 -56
- package/dist/AiSdkProvider.d.ts +18 -5
- package/dist/AiSdkProvider.d.ts.map +1 -1
- package/dist/AiSdkProvider.js +240 -153
- package/dist/AiSdkProvider.js.map +1 -1
- package/dist/Mock.d.ts +14 -15
- package/dist/Mock.d.ts.map +1 -1
- package/dist/Mock.js +26 -10
- package/dist/Mock.js.map +1 -1
- package/dist/Pool.d.ts +8 -2
- package/dist/Pool.d.ts.map +1 -1
- package/dist/Pool.js +41 -8
- package/dist/Pool.js.map +1 -1
- package/dist/ProviderRegistry.d.ts +4 -1
- package/dist/ProviderRegistry.d.ts.map +1 -1
- package/dist/ProviderRegistry.js +7 -3
- package/dist/ProviderRegistry.js.map +1 -1
- package/dist/aiSdkTransport.d.ts +4 -2
- package/dist/aiSdkTransport.d.ts.map +1 -1
- package/dist/aiSdkTransport.js +18 -3
- package/dist/aiSdkTransport.js.map +1 -1
- package/dist/catalogProvider.d.ts +3 -1
- package/dist/catalogProvider.d.ts.map +1 -1
- package/dist/catalogProvider.js +20 -7
- package/dist/catalogProvider.js.map +1 -1
- package/dist/compatibleProvider.d.ts.map +1 -1
- package/dist/compatibleProvider.js +11 -4
- package/dist/compatibleProvider.js.map +1 -1
- package/dist/cost.d.ts +11 -0
- package/dist/cost.d.ts.map +1 -0
- package/dist/cost.js +64 -0
- package/dist/cost.js.map +1 -0
- package/dist/discover.d.ts +2 -0
- package/dist/discover.d.ts.map +1 -1
- package/dist/discover.js +15 -9
- package/dist/discover.js.map +1 -1
- package/dist/env.d.ts +4 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +29 -9
- package/dist/env.js.map +1 -1
- package/dist/errors.d.ts +27 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +150 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/notices.d.ts +10 -0
- package/dist/notices.d.ts.map +1 -0
- package/dist/notices.js +11 -0
- package/dist/notices.js.map +1 -0
- package/dist/ollama.d.ts.map +1 -1
- package/dist/ollama.js +3 -3
- package/dist/ollama.js.map +1 -1
- package/dist/openai.d.ts +1 -1
- package/dist/openai.d.ts.map +1 -1
- package/dist/promptTokens.d.ts +4 -0
- package/dist/promptTokens.d.ts.map +1 -0
- package/dist/promptTokens.js +32 -0
- package/dist/promptTokens.js.map +1 -0
- package/dist/sdkModels.d.ts.map +1 -1
- package/dist/sdkModels.js +4 -3
- package/dist/sdkModels.js.map +1 -1
- package/dist/types.d.ts +43 -16
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/usage.d.ts +3 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +26 -14
- package/dist/usage.js.map +1 -1
- package/dist/warnings.js +0 -0
- package/dist/warnings.js.map +1 -1
- package/package.json +13 -9
- package/src/AiSdkProvider.test.ts +480 -159
- package/src/AiSdkProvider.ts +320 -196
- package/src/Mock.test.ts +29 -14
- package/src/Mock.ts +33 -15
- package/src/Pool.test.ts +43 -6
- package/src/Pool.ts +56 -10
- package/src/ProviderRegistry.test.ts +158 -9
- package/src/ProviderRegistry.ts +19 -6
- package/src/aiSdkTransport.ts +25 -6
- package/src/boundaries.test.ts +8 -3
- package/src/catalogProvider.test.ts +17 -0
- package/src/catalogProvider.ts +25 -10
- package/src/compatibleProvider.test.ts +96 -0
- package/src/compatibleProvider.ts +15 -6
- package/src/cost.test.ts +63 -0
- package/src/cost.ts +83 -0
- package/src/defaults.test.ts +1 -0
- package/src/discover.test.ts +48 -7
- package/src/discover.ts +31 -21
- package/src/env.test.ts +38 -23
- package/src/env.ts +45 -18
- package/src/errors.test.ts +148 -0
- package/src/errors.ts +207 -0
- package/src/index.ts +29 -7
- package/src/lexicon-guard.test.ts +6 -6
- package/src/notices.ts +22 -0
- package/src/ollama.test.ts +64 -0
- package/src/ollama.ts +6 -3
- package/src/openai.ts +3 -0
- package/src/promptTokens.ts +41 -0
- package/src/sdkModels.test.ts +7 -0
- package/src/sdkModels.ts +4 -8
- package/src/types.ts +106 -64
- package/src/usage.test.ts +15 -4
- package/src/usage.ts +32 -14
- package/src/warnings.test.ts +10 -10
- package/src/warnings.ts +0 -0
- package/dist/OpenAICompat.d.ts +0 -76
- package/dist/OpenAICompat.d.ts.map +0 -1
- package/dist/OpenAICompat.js +0 -555
- package/dist/OpenAICompat.js.map +0 -1
- package/dist/openaiStream.d.ts +0 -47
- package/dist/openaiStream.d.ts.map +0 -1
- package/dist/openaiStream.js +0 -280
- package/dist/openaiStream.js.map +0 -1
- package/dist/standardProviders.d.ts +0 -31
- package/dist/standardProviders.d.ts.map +0 -1
- package/dist/standardProviders.js +0 -518
- package/dist/standardProviders.js.map +0 -1
- package/dist/telemetry.d.ts +0 -24
- package/dist/telemetry.d.ts.map +0 -1
- package/dist/telemetry.js +0 -85
- package/dist/telemetry.js.map +0 -1
- package/src/telemetry.test.ts +0 -69
- package/src/telemetry.ts +0 -116
package/dist/telemetry.js
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
// Provider telemetry — the TelemetryEvent envelope for transport failures.
|
|
2
|
-
//
|
|
3
|
-
// TelemetryEvent is plurnk's cross-ecosystem error envelope (canonical schema:
|
|
4
|
-
// @plurnk/plurnk-grammar's schemas.plurnk.dev/v0/TelemetryEvent.json). It is
|
|
5
|
-
// mirrored here STRUCTURALLY rather than imported, so the framework keeps zero
|
|
6
|
-
// dependency on grammar (see SPEC §11). Consumers route provider events through
|
|
7
|
-
// the same `source` + `kind` discriminator as parse/rail events.
|
|
8
|
-
import { APICallError, RetryError } from "ai";
|
|
9
|
-
// Build a provider source label (`provider:<vendor>`), schema-pattern-valid.
|
|
10
|
-
export const providerSource = (vendor) => `provider:${vendor}`;
|
|
11
|
-
// A transport failure carrying its TelemetryEvent classification. IS-an Error
|
|
12
|
-
// (existing catchers keep working); telemetry-aware consumers call
|
|
13
|
-
// toTelemetryEvent() and route on source+kind.
|
|
14
|
-
export class ProviderError extends Error {
|
|
15
|
-
source;
|
|
16
|
-
kind;
|
|
17
|
-
status;
|
|
18
|
-
constructor(source, kind, message, options = {}) {
|
|
19
|
-
super(message, options.cause !== undefined ? { cause: options.cause } : undefined);
|
|
20
|
-
this.name = "ProviderError";
|
|
21
|
-
this.source = source;
|
|
22
|
-
this.kind = kind;
|
|
23
|
-
this.status = options.status ?? null;
|
|
24
|
-
}
|
|
25
|
-
toTelemetryEvent() {
|
|
26
|
-
return { source: this.source, kind: this.kind, message: this.message, position: null };
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
// An OpenAI-shaped error body carries error.type. null when the body is absent,
|
|
30
|
-
// non-JSON (a proxy/CDN HTML page), or unshaped — none of which is a match.
|
|
31
|
-
const wireErrorType = (body) => {
|
|
32
|
-
try {
|
|
33
|
-
const { error } = JSON.parse(body);
|
|
34
|
-
return typeof error?.type === "string" ? error.type : null;
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
// Map a thrown transport error to a (kind, message). Conservative; the message
|
|
41
|
-
// is factual, no guidance prose (consumer SPEC §15.1 policy).
|
|
42
|
-
export const classifyProviderError = (err) => {
|
|
43
|
-
if (RetryError.isInstance(err))
|
|
44
|
-
return classifyProviderError(err.lastError);
|
|
45
|
-
if (APICallError.isInstance(err)) {
|
|
46
|
-
const status = err.statusCode ?? 0;
|
|
47
|
-
const message = err.message;
|
|
48
|
-
const body = err.responseBody ?? "";
|
|
49
|
-
if (status === 401 || status === 403)
|
|
50
|
-
return { kind: "unauthorized", message };
|
|
51
|
-
if (status === 402)
|
|
52
|
-
return { kind: "quota_exceeded", message };
|
|
53
|
-
if (status === 429)
|
|
54
|
-
return { kind: "rate_limit", message };
|
|
55
|
-
if (status >= 500)
|
|
56
|
-
return { kind: "network_failure", message };
|
|
57
|
-
// A flagged 422 identifies a grammar-rejected exchange. It is not
|
|
58
|
-
// transport replay policy; the engine decides whether to sample again.
|
|
59
|
-
if (status === 422 && wireErrorType(body) === "grammar_invalid")
|
|
60
|
-
return { kind: "grammar_invalid", message };
|
|
61
|
-
return { kind: "invalid_response", message };
|
|
62
|
-
}
|
|
63
|
-
const wire = err;
|
|
64
|
-
if (wire?.type === "grammar_invalid") {
|
|
65
|
-
return {
|
|
66
|
-
kind: "grammar_invalid",
|
|
67
|
-
message: typeof wire.message === "string" ? wire.message : "grammar-invalid response",
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
const e = err;
|
|
71
|
-
const message = (e?.message ?? String(err)) || "request failed";
|
|
72
|
-
// Timeouts / fetch-level failures are transport, not a usable response.
|
|
73
|
-
return { kind: "network_failure", message };
|
|
74
|
-
};
|
|
75
|
-
// Wrap any thrown error as a ProviderError tagged with this provider's source.
|
|
76
|
-
// Already-classified errors pass through unchanged.
|
|
77
|
-
export const toProviderError = (err, source) => {
|
|
78
|
-
if (err instanceof ProviderError)
|
|
79
|
-
return err;
|
|
80
|
-
const underlying = RetryError.isInstance(err) ? err.lastError : err;
|
|
81
|
-
const { kind, message } = classifyProviderError(underlying);
|
|
82
|
-
const status = APICallError.isInstance(underlying) ? underlying.statusCode ?? null : null;
|
|
83
|
-
return new ProviderError(source, kind, message, { status, cause: err });
|
|
84
|
-
};
|
|
85
|
-
//# sourceMappingURL=telemetry.js.map
|
package/dist/telemetry.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"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"}
|
package/src/telemetry.test.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import { strict as assert } from "node:assert";
|
|
3
|
-
import { APICallError } from "ai";
|
|
4
|
-
import { ProviderError, classifyProviderError, toProviderError, providerSource } from "./telemetry.ts";
|
|
5
|
-
|
|
6
|
-
const apiError = (statusCode: number, responseBody = "body") => new APICallError({
|
|
7
|
-
message: `request failed (${statusCode})`,
|
|
8
|
-
url: "https://example.test/v1/chat/completions",
|
|
9
|
-
requestBodyValues: {},
|
|
10
|
-
statusCode,
|
|
11
|
-
responseBody,
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
// Schema pattern for TelemetryEvent.source (from grammar's TelemetryEvent.json).
|
|
15
|
-
const SOURCE_PATTERN = /^[a-z]+(:[a-z][a-z0-9-]*)?$/;
|
|
16
|
-
|
|
17
|
-
test("providerSource produces a schema-valid colon-namespaced source", () => {
|
|
18
|
-
assert.equal(providerSource("openai"), "provider:openai");
|
|
19
|
-
assert.match(providerSource("openrouter"), SOURCE_PATTERN);
|
|
20
|
-
assert.match(providerSource("xai"), SOURCE_PATTERN); // contains no underscore — fine
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("classifyProviderError maps HTTP status to kind", () => {
|
|
24
|
-
const k = (status: number) => classifyProviderError(apiError(status)).kind;
|
|
25
|
-
assert.equal(k(401), "unauthorized");
|
|
26
|
-
assert.equal(k(403), "unauthorized");
|
|
27
|
-
assert.equal(k(402), "quota_exceeded");
|
|
28
|
-
assert.equal(k(429), "rate_limit");
|
|
29
|
-
assert.equal(k(500), "network_failure");
|
|
30
|
-
assert.equal(k(503), "network_failure");
|
|
31
|
-
assert.equal(k(400), "invalid_response");
|
|
32
|
-
assert.equal(k(404), "invalid_response");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("classifyProviderError: a 422 flagged grammar_invalid is distinct (#548); other 422s are invalid responses", () => {
|
|
36
|
-
const rejected = apiError(422, JSON.stringify({ error: { type: "grammar_invalid", message: "non-conforming emission rejected: ..." } }));
|
|
37
|
-
assert.equal(classifyProviderError(rejected).kind, "grammar_invalid");
|
|
38
|
-
assert.equal(classifyProviderError(apiError(422, JSON.stringify({ error: { type: "invalid_request_error" } }))).kind, "invalid_response");
|
|
39
|
-
assert.equal(classifyProviderError(apiError(422, "<html>Bad</html>")).kind, "invalid_response");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("classifyProviderError treats non-HTTP errors as network_failure", () => {
|
|
43
|
-
assert.equal(classifyProviderError(new TypeError("fetch failed")).kind, "network_failure");
|
|
44
|
-
const timeout = Object.assign(new Error("timed out"), { name: "TimeoutError" });
|
|
45
|
-
assert.equal(classifyProviderError(timeout).kind, "network_failure");
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("ProviderError.toTelemetryEvent emits the canonical envelope", () => {
|
|
49
|
-
const e = new ProviderError("provider:openai", "rate_limit", "OpenAI 429 - slow down", { status: 429 });
|
|
50
|
-
const ev = e.toTelemetryEvent();
|
|
51
|
-
assert.deepEqual(ev, { source: "provider:openai", kind: "rate_limit", message: "OpenAI 429 - slow down", position: null });
|
|
52
|
-
assert.match(ev.source, SOURCE_PATTERN);
|
|
53
|
-
assert.ok(e instanceof Error); // still catchable as a plain Error
|
|
54
|
-
assert.equal(e.status, 429);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("toProviderError classifies and tags an HTTP error with the source + status", () => {
|
|
58
|
-
const cause = apiError(401, "no key");
|
|
59
|
-
const pe = toProviderError(cause, "provider:groq");
|
|
60
|
-
assert.equal(pe.kind, "unauthorized");
|
|
61
|
-
assert.equal(pe.source, "provider:groq");
|
|
62
|
-
assert.equal(pe.status, 401);
|
|
63
|
-
assert.equal(pe.cause, cause);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("toProviderError passes an existing ProviderError through unchanged", () => {
|
|
67
|
-
const original = new ProviderError("provider:xai", "rate_limit", "429");
|
|
68
|
-
assert.equal(toProviderError(original, "provider:other"), original);
|
|
69
|
-
});
|
package/src/telemetry.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
// Provider telemetry — the TelemetryEvent envelope for transport failures.
|
|
2
|
-
//
|
|
3
|
-
// TelemetryEvent is plurnk's cross-ecosystem error envelope (canonical schema:
|
|
4
|
-
// @plurnk/plurnk-grammar's schemas.plurnk.dev/v0/TelemetryEvent.json). It is
|
|
5
|
-
// mirrored here STRUCTURALLY rather than imported, so the framework keeps zero
|
|
6
|
-
// dependency on grammar (see SPEC §11). Consumers route provider events through
|
|
7
|
-
// the same `source` + `kind` discriminator as parse/rail events.
|
|
8
|
-
|
|
9
|
-
import { APICallError, RetryError } from "ai";
|
|
10
|
-
|
|
11
|
-
// Required by the schema: source (producer id) + kind (discriminator). message
|
|
12
|
-
// and position are optional. A transport failure isn't localizable, so it carries
|
|
13
|
-
// a null position; a `grammar_unenforced` event (#24) carries the divergence
|
|
14
|
-
// code-point offset into the model's emission, so the consumer can render a
|
|
15
|
-
// snippet around it — same recovery affordance the consumer already gives DSL
|
|
16
|
-
// parse errors.
|
|
17
|
-
export type TelemetryEvent = {
|
|
18
|
-
source: string; // e.g. "provider:openai" — schema pattern ^[a-z]+(:[a-z][a-z0-9-]*)?$
|
|
19
|
-
kind: string; // open vocabulary; providers mint from ProviderTelemetryKind
|
|
20
|
-
message?: string | null;
|
|
21
|
-
position?: number | null;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
// The kinds plurnk-service routes provider failures on.
|
|
25
|
-
export type ProviderTelemetryKind =
|
|
26
|
-
| "rate_limit"
|
|
27
|
-
| "network_failure"
|
|
28
|
-
| "model_refused"
|
|
29
|
-
| "invalid_response"
|
|
30
|
-
| "unauthorized"
|
|
31
|
-
| "quota_exceeded"
|
|
32
|
-
// A 422 whose error.type is "grammar_invalid" (#548): the backend served one
|
|
33
|
-
// generation and rejected it as non-conforming. This identifies the failure;
|
|
34
|
-
// whether to start another model exchange belongs to the engine.
|
|
35
|
-
| "grammar_invalid"
|
|
36
|
-
// Output did not conform to the GBNF. ALWAYS an observation, never a throw:
|
|
37
|
-
// a completed exchange returns its bytes with this event on
|
|
38
|
-
// response.telemetry (message + divergence position), whether the grammar
|
|
39
|
-
// was transported or withheld (filter mode). Discard/retry/escalate is
|
|
40
|
-
// consumer policy (#24, SPEC §13).
|
|
41
|
-
| "grammar_unenforced";
|
|
42
|
-
|
|
43
|
-
// Build a provider source label (`provider:<vendor>`), schema-pattern-valid.
|
|
44
|
-
export const providerSource = (vendor: string): string => `provider:${vendor}`;
|
|
45
|
-
|
|
46
|
-
// A transport failure carrying its TelemetryEvent classification. IS-an Error
|
|
47
|
-
// (existing catchers keep working); telemetry-aware consumers call
|
|
48
|
-
// toTelemetryEvent() and route on source+kind.
|
|
49
|
-
export class ProviderError extends Error {
|
|
50
|
-
readonly source: string;
|
|
51
|
-
readonly kind: ProviderTelemetryKind;
|
|
52
|
-
readonly status: number | null;
|
|
53
|
-
|
|
54
|
-
constructor(source: string, kind: ProviderTelemetryKind, message: string, options: { status?: number | null; cause?: unknown } = {}) {
|
|
55
|
-
super(message, options.cause !== undefined ? { cause: options.cause } : undefined);
|
|
56
|
-
this.name = "ProviderError";
|
|
57
|
-
this.source = source;
|
|
58
|
-
this.kind = kind;
|
|
59
|
-
this.status = options.status ?? null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
toTelemetryEvent(): TelemetryEvent {
|
|
63
|
-
return { source: this.source, kind: this.kind, message: this.message, position: null };
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// An OpenAI-shaped error body carries error.type. null when the body is absent,
|
|
68
|
-
// non-JSON (a proxy/CDN HTML page), or unshaped — none of which is a match.
|
|
69
|
-
const wireErrorType = (body: string): string | null => {
|
|
70
|
-
try {
|
|
71
|
-
const { error } = JSON.parse(body) as { error?: { type?: unknown } };
|
|
72
|
-
return typeof error?.type === "string" ? error.type : null;
|
|
73
|
-
} catch {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
// Map a thrown transport error to a (kind, message). Conservative; the message
|
|
79
|
-
// is factual, no guidance prose (consumer SPEC §15.1 policy).
|
|
80
|
-
export const classifyProviderError = (err: unknown): { kind: ProviderTelemetryKind; message: string } => {
|
|
81
|
-
if (RetryError.isInstance(err)) return classifyProviderError(err.lastError);
|
|
82
|
-
if (APICallError.isInstance(err)) {
|
|
83
|
-
const status = err.statusCode ?? 0;
|
|
84
|
-
const message = err.message;
|
|
85
|
-
const body = err.responseBody ?? "";
|
|
86
|
-
if (status === 401 || status === 403) return { kind: "unauthorized", message };
|
|
87
|
-
if (status === 402) return { kind: "quota_exceeded", message };
|
|
88
|
-
if (status === 429) return { kind: "rate_limit", message };
|
|
89
|
-
if (status >= 500) return { kind: "network_failure", message };
|
|
90
|
-
// A flagged 422 identifies a grammar-rejected exchange. It is not
|
|
91
|
-
// transport replay policy; the engine decides whether to sample again.
|
|
92
|
-
if (status === 422 && wireErrorType(body) === "grammar_invalid") return { kind: "grammar_invalid", message };
|
|
93
|
-
return { kind: "invalid_response", message };
|
|
94
|
-
}
|
|
95
|
-
const wire = err as { message?: unknown; type?: unknown; status?: unknown };
|
|
96
|
-
if (wire?.type === "grammar_invalid") {
|
|
97
|
-
return {
|
|
98
|
-
kind: "grammar_invalid",
|
|
99
|
-
message: typeof wire.message === "string" ? wire.message : "grammar-invalid response",
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
const e = err as { name?: string; message?: string };
|
|
103
|
-
const message = (e?.message ?? String(err)) || "request failed";
|
|
104
|
-
// Timeouts / fetch-level failures are transport, not a usable response.
|
|
105
|
-
return { kind: "network_failure", message };
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
// Wrap any thrown error as a ProviderError tagged with this provider's source.
|
|
109
|
-
// Already-classified errors pass through unchanged.
|
|
110
|
-
export const toProviderError = (err: unknown, source: string): ProviderError => {
|
|
111
|
-
if (err instanceof ProviderError) return err;
|
|
112
|
-
const underlying = RetryError.isInstance(err) ? err.lastError : err;
|
|
113
|
-
const { kind, message } = classifyProviderError(underlying);
|
|
114
|
-
const status = APICallError.isInstance(underlying) ? underlying.statusCode ?? null : null;
|
|
115
|
-
return new ProviderError(source, kind, message, { status, cause: err });
|
|
116
|
-
};
|