@north-light/api 0.3.445 → 0.3.447

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.
@@ -0,0 +1,74 @@
1
+ function text(value) {
2
+ return typeof value === 'string' ? value : '';
3
+ }
4
+ function signal(kind, matchedOn, options = {}) {
5
+ return { kind, matchedOn, ...options };
6
+ }
7
+ function withTextMatch(signal, textSignal) {
8
+ return textSignal.matchedOn === 'text'
9
+ ? { ...signal, textKind: textSignal.kind, textMatch: textSignal.match, textMatches: textSignal.textMatches }
10
+ : signal;
11
+ }
12
+ const TEXT_PATTERNS = [
13
+ ['rate-limit', /rate.?limit|\b429\b|too many requests|usage[ _]limit|usage_not_included|usagelimiterror|insufficient_quota|quota exceeded/i],
14
+ ['protocol', /\b413\b|payload too large|entity too large|request too large|entity\.too\.large|body exceeds/i],
15
+ ['auth', /invalid_grant|refresh token|unauthori[sz]ed|invalid.?api.?key|authentication failed|\b401\b|\b403\b/i],
16
+ ['context-overflow', /context overflow recovery failed/i],
17
+ ['protocol', /websocket.*(?:closed.*\b(?:1002|1003|1007|1008|1009|1010)\b|protocol error|policy violation|unsupported data|invalid (?:frame|payload)|message too big|mandatory extension)/i],
18
+ ['overloaded-only', /lock file is already being held/i],
19
+ ['overloaded', /overloaded|temporarily unavailable/i],
20
+ ['service-unavailable', /service.?unavailable/i],
21
+ ['overloaded-only', /internal server error|api.?error|\b5\d\d\b|capacity|server.{0,3}busy/i],
22
+ ['connection', /connection|econnreset|etimedout|enotfound|econnrefused|network|fetch failed|socket hang|timed? out|timeout|websocket\s+(?:was\s+)?closed|websocket stream closed before response\.completed|websocket\s+error|websocket.*interrupt/i],
23
+ ['socket', /socket/i],
24
+ ['connection-only', /eai_again|epipe|ehostunreach|enetunreach|\bterminated\b|other side closed|premature close/i],
25
+ ];
26
+ function recognizeText(message) {
27
+ // The order preserves the canonical taxonomy: size wins over wrapped 5xx prose,
28
+ // and auth wins over a generic API-error wrapper. Every match remains available
29
+ // because rotation deliberately gives transient text priority over rate-limit text.
30
+ const matches = TEXT_PATTERNS.flatMap(([kind, pattern]) => {
31
+ const match = message.match(pattern);
32
+ return match ? [{ kind, match: match[0] }] : [];
33
+ });
34
+ const first = matches[0];
35
+ return first === undefined ? signal('other', 'none') : { ...signal(first.kind, 'text', { match: first.match }), textMatches: matches };
36
+ }
37
+ /** Recognizes provider failure facts. Consumers apply their own retry policy. */
38
+ export function recognizeProviderError(input) {
39
+ const retryAfterMs = input.retryAfterMs;
40
+ const textSignal = recognizeText(input.message ?? '');
41
+ const errno = text(input.code) || text(input.errno);
42
+ if (/^(ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOENT|ENOTFOUND|EAI_AGAIN|EPIPE|EHOSTUNREACH|ENETUNREACH)$/i.test(errno)) {
43
+ return withTextMatch(signal('connection', 'errno', { retryAfterMs }), textSignal);
44
+ }
45
+ if (/^ELOCKED$/i.test(errno))
46
+ return withTextMatch(signal('overloaded', 'errno', { retryAfterMs }), textSignal);
47
+ const status = typeof input.status === 'number' ? input.status : undefined;
48
+ if (status !== undefined) {
49
+ if (status === 401 || status === 403)
50
+ return withTextMatch(signal('auth', 'status', { status, retryAfterMs }), textSignal);
51
+ if (status === 429)
52
+ return withTextMatch(signal('rate-limit', 'status', { status, retryAfterMs }), textSignal);
53
+ if (status === 500 || status === 502 || status === 503 || status === 504)
54
+ return withTextMatch(signal('transient-http', 'status', { status, retryAfterMs }), textSignal);
55
+ if (status >= 500 && status <= 599)
56
+ return withTextMatch(signal('overloaded', 'status', { status, retryAfterMs }), textSignal);
57
+ if (status >= 400 && status <= 499)
58
+ return withTextMatch(signal('protocol', 'status', { status, retryAfterMs }), textSignal);
59
+ }
60
+ const wsCloseCode = typeof input.wsCloseCode === 'number' ? input.wsCloseCode : undefined;
61
+ if (wsCloseCode === 1008)
62
+ return withTextMatch(signal('protocol', 'websocket-close', { retryAfterMs }), textSignal);
63
+ if (wsCloseCode === 1006)
64
+ return withTextMatch(signal('connection', 'websocket-close', { retryAfterMs }), textSignal);
65
+ if (wsCloseCode === 1011) {
66
+ const reason = text(input.wsCloseReason);
67
+ const reasonSignal = recognizeText(reason);
68
+ if (/no running broker/i.test(reason) || reasonSignal.kind === 'connection' || reasonSignal.kind === 'connection-only') {
69
+ return withTextMatch(signal('connection', 'websocket-close', { retryAfterMs }), textSignal);
70
+ }
71
+ return withTextMatch(signal('other', 'websocket-close', { retryAfterMs }), textSignal);
72
+ }
73
+ return retryAfterMs === undefined ? textSignal : { ...textSignal, retryAfterMs };
74
+ }
@@ -0,0 +1,27 @@
1
+ import type { AssistantMessageDiagnostic } from '@earendil-works/pi-ai';
2
+ export declare const PROVIDER_FAILURE_DIAGNOSTIC = "crtr:provider-failure";
3
+ export interface ProviderFailureCause {
4
+ message?: string;
5
+ status?: number;
6
+ code?: string | number;
7
+ errno?: string;
8
+ body?: string;
9
+ }
10
+ export interface ProviderFailureDetails {
11
+ status?: number;
12
+ code?: string | number;
13
+ errno?: string;
14
+ body?: string;
15
+ causes?: ProviderFailureCause[];
16
+ }
17
+ export interface NormalizedProviderFailure extends ProviderFailureDetails {
18
+ message: string;
19
+ }
20
+ /** Converts an arbitrary provider failure into serializable facts before an SDK stream flattens it to `errorMessage`. */
21
+ export declare function normalizeProviderFailure(error: unknown, observed?: {
22
+ status?: number;
23
+ }): NormalizedProviderFailure;
24
+ /** The stable diagnostic carrier for normalized provider error facts on an assistant error message. */
25
+ export declare function providerFailureDiagnostic(details: ProviderFailureDetails): AssistantMessageDiagnostic;
26
+ /** Reads provider facts from the assistant-message diagnostic without trusting arbitrary nested error objects. */
27
+ export declare function extractProviderFailureDetails(message: unknown): ProviderFailureDetails | undefined;
@@ -0,0 +1,109 @@
1
+ import { isRecord } from '../shared/predicates.js';
2
+ export const PROVIDER_FAILURE_DIAGNOSTIC = 'crtr:provider-failure';
3
+ const CAUSE_LIMIT = 8;
4
+ function text(value) {
5
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
6
+ }
7
+ function status(value) {
8
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
9
+ }
10
+ function code(value) {
11
+ return typeof value === 'string' || typeof value === 'number' ? value : undefined;
12
+ }
13
+ function record(value) {
14
+ return isRecord(value) ? value : undefined;
15
+ }
16
+ function failureCause(value) {
17
+ const message = text(value['errorMessage']) ?? text(value['message']) ?? text(value['reason']) ?? text(value['statusText']);
18
+ const cause = {
19
+ ...(message === undefined ? {} : { message }),
20
+ ...(status(value['status']) === undefined ? {} : { status: status(value['status']) }),
21
+ ...(code(value['code']) === undefined ? {} : { code: code(value['code']) }),
22
+ ...(text(value['errno']) === undefined ? {} : { errno: text(value['errno']) }),
23
+ ...(text(value['body']) === undefined ? {} : { body: text(value['body']) }),
24
+ };
25
+ return Object.keys(cause).length === 0 ? undefined : cause;
26
+ }
27
+ /** Converts an arbitrary provider failure into serializable facts before an SDK stream flattens it to `errorMessage`. */
28
+ export function normalizeProviderFailure(error, observed = {}) {
29
+ const root = record(error);
30
+ const rootCause = root === undefined ? undefined : failureCause(root);
31
+ const causes = [];
32
+ const seen = new Set([error]);
33
+ let current = root === undefined ? undefined : record(root['cause']);
34
+ while (current !== undefined && !seen.has(current) && causes.length < CAUSE_LIMIT) {
35
+ seen.add(current);
36
+ const cause = failureCause(current);
37
+ if (cause !== undefined)
38
+ causes.push(cause);
39
+ current = record(current['cause']);
40
+ }
41
+ const message = rootCause?.message ?? (error instanceof Error ? error.message : undefined) ?? String(error ?? 'Provider request failed');
42
+ return {
43
+ message,
44
+ ...((observed.status ?? rootCause?.status) === undefined ? {} : { status: observed.status ?? rootCause?.status }),
45
+ ...(rootCause?.code === undefined ? {} : { code: rootCause.code }),
46
+ ...(rootCause?.errno === undefined ? {} : { errno: rootCause.errno }),
47
+ ...(rootCause?.body === undefined ? {} : { body: rootCause.body }),
48
+ ...(causes.length === 0 ? {} : { causes }),
49
+ };
50
+ }
51
+ /** The stable diagnostic carrier for normalized provider error facts on an assistant error message. */
52
+ export function providerFailureDiagnostic(details) {
53
+ return { type: PROVIDER_FAILURE_DIAGNOSTIC, timestamp: Date.now(), details: jsonDetails(details) };
54
+ }
55
+ /** pi types diagnostic details as strict JSON, which excludes `undefined`; every
56
+ * field here is a JSON scalar or a list of them by declaration, so dropping the
57
+ * absent keys is the only conversion needed. */
58
+ function jsonDetails(details) {
59
+ const out = jsonCause(details);
60
+ if (details.causes !== undefined)
61
+ out['causes'] = details.causes.map(jsonCause);
62
+ return out;
63
+ }
64
+ function jsonCause(cause) {
65
+ const out = {};
66
+ if (cause.message !== undefined)
67
+ out['message'] = cause.message;
68
+ if (cause.status !== undefined)
69
+ out['status'] = cause.status;
70
+ if (cause.code !== undefined)
71
+ out['code'] = cause.code;
72
+ if (cause.errno !== undefined)
73
+ out['errno'] = cause.errno;
74
+ if (cause.body !== undefined)
75
+ out['body'] = cause.body;
76
+ return out;
77
+ }
78
+ function providerFailureDetails(value) {
79
+ const diagnostics = record(value)?.['diagnostics'];
80
+ if (!Array.isArray(diagnostics))
81
+ return undefined;
82
+ for (const diagnostic of diagnostics) {
83
+ const diagnosticRecord = record(diagnostic);
84
+ if (diagnosticRecord?.['type'] !== PROVIDER_FAILURE_DIAGNOSTIC)
85
+ continue;
86
+ const details = record(diagnosticRecord['details']);
87
+ if (details === undefined)
88
+ continue;
89
+ const causes = Array.isArray(details['causes'])
90
+ ? details['causes'].flatMap((item) => {
91
+ const cause = record(item);
92
+ const normalized = cause === undefined ? undefined : failureCause(cause);
93
+ return normalized === undefined ? [] : [normalized];
94
+ })
95
+ : undefined;
96
+ return {
97
+ ...(status(details['status']) === undefined ? {} : { status: status(details['status']) }),
98
+ ...(code(details['code']) === undefined ? {} : { code: code(details['code']) }),
99
+ ...(text(details['errno']) === undefined ? {} : { errno: text(details['errno']) }),
100
+ ...(text(details['body']) === undefined ? {} : { body: text(details['body']) }),
101
+ ...(causes === undefined || causes.length === 0 ? {} : { causes }),
102
+ };
103
+ }
104
+ return undefined;
105
+ }
106
+ /** Reads provider facts from the assistant-message diagnostic without trusting arbitrary nested error objects. */
107
+ export function extractProviderFailureDetails(message) {
108
+ return providerFailureDetails(message);
109
+ }