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.
@@ -1,12 +1,17 @@
1
1
  /**
2
2
  * Shared error classification for provider adapters.
3
3
  *
4
- * Maps SDK errors (by HTTP status) to a canonical `TypedError` with
5
- * stable `type` tags (AUTH_INVALID, RATE_LIMIT, PROVIDER_COOLDOWN,
6
- * PROVIDER_UNAVAILABLE, …). 401/403 messages stay generic to avoid
7
- * echoing provider bodies that may contain the API key back on the
8
- * wire; for other statuses the provider's own detail is preserved
9
- * on `.detail` so callers can debug 400s (schema rejects, etc).
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: extractDetail(err)
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: extractDetail(err)
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: extractDetail(err)
177
+ detail
76
178
  }
77
179
  }
78
180
  return {
79
- message: err?.message ? String(err.message).slice(0, 200) : 'network error',
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: extractDetail(err)
185
+ detail
84
186
  }
85
187
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mohdel",
3
- "version": "0.95.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.95.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
- }