mohdel 0.95.0 → 0.97.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/js/session/adapters/_errors.js +115 -13
- package/package.json +2 -2
- package/src/lib/errors.js +0 -83
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared error classification for provider adapters.
|
|
3
3
|
*
|
|
4
|
-
* Maps SDK errors
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* on
|
|
4
|
+
* Maps SDK errors to a canonical `TypedError`. Inspects provider
|
|
5
|
+
* error codes / messages first (so semantically meaningful tags like
|
|
6
|
+
* `CONTEXT_OVERFLOW` and `QUOTA_EXHAUSTED` aren't lost in generic
|
|
7
|
+
* status buckets), then falls back to HTTP status. 401/403 messages
|
|
8
|
+
* stay generic to avoid echoing provider bodies that may contain the
|
|
9
|
+
* API key back on the wire; for other statuses the provider's own
|
|
10
|
+
* detail is preserved on `.detail` so callers can debug 400s.
|
|
11
|
+
*
|
|
12
|
+
* Type tags: `AUTH_INVALID`, `RATE_LIMIT`, `QUOTA_EXHAUSTED`,
|
|
13
|
+
* `CONTEXT_OVERFLOW`, `CONTENT_BLOCKED`, `PROVIDER_UNAVAILABLE`,
|
|
14
|
+
* `PROVIDER_ERROR`, `NET_ERROR`.
|
|
10
15
|
*
|
|
11
16
|
* @module session/adapters/_errors
|
|
12
17
|
*/
|
|
@@ -22,8 +27,6 @@ const DETAIL_CAP = 500
|
|
|
22
27
|
*/
|
|
23
28
|
function extractDetail (err) {
|
|
24
29
|
if (!err) return undefined
|
|
25
|
-
// OpenAI SDK: err.error.message; Google SDK: err.message is the full
|
|
26
|
-
// body sometimes. Prefer the structured field when present.
|
|
27
30
|
const nested = err.error?.message || err.response?.data?.error?.message
|
|
28
31
|
const raw = nested || err.message
|
|
29
32
|
if (!raw) return undefined
|
|
@@ -31,6 +34,49 @@ function extractDetail (err) {
|
|
|
31
34
|
return str.length > DETAIL_CAP ? str.slice(0, DETAIL_CAP) + '…' : str
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Pull a provider-supplied error code/type out of any of the shapes
|
|
39
|
+
* the SDKs use. OpenAI/xAI/DeepSeek expose `err.code` or
|
|
40
|
+
* `err.error.code`; Anthropic surfaces `err.error.error.type`;
|
|
41
|
+
* Gemini buries everything in `err.message`. Lower-cased for a
|
|
42
|
+
* single substring/equality check site.
|
|
43
|
+
* @param {any} err
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
function extractCode (err) {
|
|
47
|
+
if (!err) return ''
|
|
48
|
+
const candidates = [
|
|
49
|
+
err.code,
|
|
50
|
+
err.error?.code,
|
|
51
|
+
err.error?.type,
|
|
52
|
+
err.error?.error?.type,
|
|
53
|
+
err.error?.error?.code,
|
|
54
|
+
err.response?.data?.error?.code,
|
|
55
|
+
err.response?.data?.error?.type
|
|
56
|
+
]
|
|
57
|
+
for (const c of candidates) {
|
|
58
|
+
if (typeof c === 'string' && c) return c.toLowerCase()
|
|
59
|
+
}
|
|
60
|
+
return ''
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Match common context-overflow phrasings used by providers that
|
|
65
|
+
* don't expose a dedicated error code (Gemini, some compat gateways).
|
|
66
|
+
* @param {string} msg
|
|
67
|
+
* @returns {boolean}
|
|
68
|
+
*/
|
|
69
|
+
function matchesContextOverflow (msg) {
|
|
70
|
+
const m = (msg || '').toLowerCase()
|
|
71
|
+
if (!m) return false
|
|
72
|
+
if (m.includes('context_length') || m.includes('context length')) return true
|
|
73
|
+
if (m.includes('maximum context') || m.includes('context window')) return true
|
|
74
|
+
if (m.includes('prompt is too long') || m.includes('input is too long')) return true
|
|
75
|
+
if (m.includes('too many tokens') || m.includes('token limit')) return true
|
|
76
|
+
if (m.includes('max_tokens') && m.includes('exceed')) return true
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
|
|
34
80
|
/**
|
|
35
81
|
* @param {unknown} e
|
|
36
82
|
* @returns {import('#core/errors.js').TypedError}
|
|
@@ -38,6 +84,62 @@ function extractDetail (err) {
|
|
|
38
84
|
export function classifyProviderError (e) {
|
|
39
85
|
const err = /** @type {any} */(e)
|
|
40
86
|
const status = err?.status
|
|
87
|
+
const code = extractCode(err)
|
|
88
|
+
const message = err?.message || ''
|
|
89
|
+
const detail = extractDetail(err)
|
|
90
|
+
|
|
91
|
+
// --- Code-driven classification (runs before status buckets so
|
|
92
|
+
// specific tags survive even when the upstream HTTP status is
|
|
93
|
+
// a generic 400/429). ---
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
code === 'context_length_exceeded' ||
|
|
97
|
+
code === 'string_above_max_length' ||
|
|
98
|
+
code === 'context_length' ||
|
|
99
|
+
matchesContextOverflow(message) ||
|
|
100
|
+
matchesContextOverflow(detail)
|
|
101
|
+
) {
|
|
102
|
+
return {
|
|
103
|
+
message: 'context length exceeded',
|
|
104
|
+
severity: 'warn',
|
|
105
|
+
retryable: false,
|
|
106
|
+
type: 'CONTEXT_OVERFLOW',
|
|
107
|
+
detail
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (
|
|
112
|
+
code === 'insufficient_quota' ||
|
|
113
|
+
code === 'billing_hard_limit_reached' ||
|
|
114
|
+
code === 'account_deactivated' ||
|
|
115
|
+
code === 'credit_balance_too_low'
|
|
116
|
+
) {
|
|
117
|
+
return {
|
|
118
|
+
message: 'provider quota exhausted',
|
|
119
|
+
severity: 'error',
|
|
120
|
+
retryable: false,
|
|
121
|
+
type: 'QUOTA_EXHAUSTED',
|
|
122
|
+
detail
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (
|
|
127
|
+
code === 'content_filter' ||
|
|
128
|
+
code === 'content_policy_violation' ||
|
|
129
|
+
code === 'safety' ||
|
|
130
|
+
code === 'blocked' ||
|
|
131
|
+
code === 'prohibited_content'
|
|
132
|
+
) {
|
|
133
|
+
return {
|
|
134
|
+
message: 'content blocked by provider safety filter',
|
|
135
|
+
severity: 'warn',
|
|
136
|
+
retryable: false,
|
|
137
|
+
type: 'CONTENT_BLOCKED',
|
|
138
|
+
detail
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- Status-driven fallback. ---
|
|
41
143
|
|
|
42
144
|
if (status === 401 || status === 403) {
|
|
43
145
|
// Deliberately no detail — 401/403 bodies can echo the key.
|
|
@@ -54,7 +156,7 @@ export function classifyProviderError (e) {
|
|
|
54
156
|
severity: 'warn',
|
|
55
157
|
retryable: true,
|
|
56
158
|
type: 'RATE_LIMIT',
|
|
57
|
-
detail
|
|
159
|
+
detail
|
|
58
160
|
}
|
|
59
161
|
}
|
|
60
162
|
if (typeof status === 'number' && status >= 500) {
|
|
@@ -63,7 +165,7 @@ export function classifyProviderError (e) {
|
|
|
63
165
|
severity: 'warn',
|
|
64
166
|
retryable: true,
|
|
65
167
|
type: 'PROVIDER_UNAVAILABLE',
|
|
66
|
-
detail
|
|
168
|
+
detail
|
|
67
169
|
}
|
|
68
170
|
}
|
|
69
171
|
if (typeof status === 'number' && status >= 400) {
|
|
@@ -72,14 +174,14 @@ export function classifyProviderError (e) {
|
|
|
72
174
|
severity: 'error',
|
|
73
175
|
retryable: false,
|
|
74
176
|
type: 'PROVIDER_ERROR',
|
|
75
|
-
detail
|
|
177
|
+
detail
|
|
76
178
|
}
|
|
77
179
|
}
|
|
78
180
|
return {
|
|
79
|
-
message:
|
|
181
|
+
message: message ? String(message).slice(0, 200) : 'network error',
|
|
80
182
|
severity: 'warn',
|
|
81
183
|
retryable: true,
|
|
82
184
|
type: 'NET_ERROR',
|
|
83
|
-
detail
|
|
185
|
+
detail
|
|
84
186
|
}
|
|
85
187
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mohdel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.97.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Christophe Le Bars",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"@opentelemetry/exporter-trace-otlp-grpc": "^0.215.0",
|
|
88
88
|
"@opentelemetry/sdk-node": "^0.215.0",
|
|
89
89
|
"chalk": "^5.4.0",
|
|
90
|
-
"mohdel-thin-gate-linux-x64-gnu": "0.
|
|
90
|
+
"mohdel-thin-gate-linux-x64-gnu": "0.97.0"
|
|
91
91
|
},
|
|
92
92
|
"dependencies": {
|
|
93
93
|
"@anthropic-ai/sdk": "^0.91.0",
|
package/src/lib/errors.js
CHANGED
|
@@ -26,16 +26,6 @@ export const getSeverityNumber = (severitySymbol) => {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
// NOTE used to mock upstream Error
|
|
30
|
-
export class APIError extends Error {
|
|
31
|
-
constructor (message, status = 500) {
|
|
32
|
-
super(message)
|
|
33
|
-
this.name = 'APIError'
|
|
34
|
-
this.status = status
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// usually Severity at least info
|
|
39
29
|
export class MohdelError extends Error {
|
|
40
30
|
constructor (
|
|
41
31
|
message,
|
|
@@ -51,76 +41,3 @@ export class MohdelError extends Error {
|
|
|
51
41
|
this.silent = silent
|
|
52
42
|
}
|
|
53
43
|
}
|
|
54
|
-
|
|
55
|
-
// Convert any error to the serialized transport shape. Duck-types on
|
|
56
|
-
// `detail` to distinguish typed errors (MohdelError) from plain Error.
|
|
57
|
-
export const toTransportError = (err, span) => {
|
|
58
|
-
const isTyped = err.detail !== undefined
|
|
59
|
-
return {
|
|
60
|
-
message: isTyped ? err.message : 'UNEXPECTED_ERROR',
|
|
61
|
-
detail: isTyped ? err.detail : 'An unexpected error occurred',
|
|
62
|
-
trace: span?.spanContext()?.traceId,
|
|
63
|
-
component: err.component || undefined,
|
|
64
|
-
context: err.context || undefined,
|
|
65
|
-
retryable: err.retryable ?? false,
|
|
66
|
-
silent: err.silent ?? false
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export const retryableWarn = (err, detail) => {
|
|
71
|
-
return {
|
|
72
|
-
message: 'PROVIDER_OVERLOADED',
|
|
73
|
-
severity: Severity.WARN,
|
|
74
|
-
retryable: true,
|
|
75
|
-
detail,
|
|
76
|
-
cause: err
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
export const reportRetryable = (err, provider, detail) => {
|
|
80
|
-
detail ||= `**An unexpected error occurred**: ${provider}'s API failed to respond. Try again or switch to a different model. If the issue persists, please contact support and provide the Trace ID.`
|
|
81
|
-
return {
|
|
82
|
-
message: 'PROVIDER_RETRYABLE_ERROR',
|
|
83
|
-
severity: Severity.ERROR,
|
|
84
|
-
detail,
|
|
85
|
-
retryable: true,
|
|
86
|
-
cause: err
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export const reportDefault = (err, provider) => {
|
|
91
|
-
return {
|
|
92
|
-
message: 'PROVIDER_ERROR',
|
|
93
|
-
severity: Severity.ERROR,
|
|
94
|
-
detail: `**An unexpected error occurred**: ${provider}'s API failed to respond. Try switching to a different model or please contact support and provide the Trace ID.`,
|
|
95
|
-
retryable: isConnectionError(err),
|
|
96
|
-
cause: err
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export const reportContextOverflow = (err, provider) => {
|
|
101
|
-
return {
|
|
102
|
-
message: 'CONTEXT_OVERFLOW',
|
|
103
|
-
severity: Severity.WARN,
|
|
104
|
-
detail: `The prompt exceeds ${provider}'s context limit. Reduce input or switch to a larger context model.`,
|
|
105
|
-
retryable: false,
|
|
106
|
-
cause: err
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function isContextOverflowMessage (errMessage) {
|
|
111
|
-
const msg = (errMessage || '').toLowerCase()
|
|
112
|
-
if (msg.includes('context_length') || msg.includes('context length')) return true
|
|
113
|
-
if (msg.includes('token limit') || msg.includes('too long') || msg.includes('too many tokens')) return true
|
|
114
|
-
if (msg.includes('maximum context') || msg.includes('prompt is too long')) return true
|
|
115
|
-
if (msg.includes('max_tokens') && msg.includes('exceed')) return true
|
|
116
|
-
return false
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function isConnectionError (err) {
|
|
120
|
-
const code = err?.code || err?.cause?.code
|
|
121
|
-
if (code === 'ECONNRESET' || code === 'ECONNREFUSED' || code === 'ETIMEDOUT' ||
|
|
122
|
-
code === 'EPIPE' || code === 'ENOTFOUND' || code === 'UND_ERR_CONNECT_TIMEOUT') return true
|
|
123
|
-
const msg = (err?.message || '').toLowerCase()
|
|
124
|
-
if (msg.includes('fetch failed') || msg.includes('socket hang up') || msg.includes('network')) return true
|
|
125
|
-
return false
|
|
126
|
-
}
|