mohdel 0.98.1 → 0.99.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.
@@ -95,7 +95,7 @@ export async function * runChatCompletions (envelope, client, config, deps = {})
95
95
  response = await client.chat.completions.create(args, { signal: deps.signal })
96
96
  } catch (e) {
97
97
  deps.log?.warn({ err: e }, `[mohdel:${config.provider}] request failed`)
98
- yield { type: 'error', error: classifyProviderError(e) }
98
+ yield { type: 'error', error: classifyProviderError(e, envelope.auth?.key) }
99
99
  return
100
100
  }
101
101
 
@@ -158,7 +158,7 @@ async function * runStreaming (envelope, client, args, config, start, deps) {
158
158
  stream = await client.chat.completions.create(args, { signal: deps.signal })
159
159
  } catch (e) {
160
160
  deps.log?.warn({ err: e }, `[mohdel:${config.provider}] request failed`)
161
- yield { type: 'error', error: classifyProviderError(e) }
161
+ yield { type: 'error', error: classifyProviderError(e, envelope.auth?.key) }
162
162
  return
163
163
  }
164
164
 
@@ -222,7 +222,7 @@ async function * runStreaming (envelope, client, args, config, start, deps) {
222
222
  }
223
223
  } catch (e) {
224
224
  deps.log?.warn({ err: e }, `[mohdel:${config.provider}] stream failed`)
225
- yield { type: 'error', error: classifyProviderError(e) }
225
+ yield { type: 'error', error: classifyProviderError(e, envelope.auth?.key) }
226
226
  return
227
227
  }
228
228
 
@@ -4,10 +4,14 @@
4
4
  * Maps SDK errors to a canonical `TypedError`. Inspects provider
5
5
  * error codes / messages first (so semantically meaningful tags like
6
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.
7
+ * status buckets), then falls back to HTTP status. The provider's own
8
+ * detail is preserved on `.detail` for every classification
9
+ * including 401/403. When the caller supplies the API key it was
10
+ * using, `classifyProviderError` masks any verbatim occurrence of
11
+ * that key in the detail before returning, so an echoed-key provider
12
+ * body never reaches downstream consumers as plaintext. Whatever the
13
+ * caller does with `detail` after that — surface it, log it, redact
14
+ * it further — is the caller's policy.
11
15
  *
12
16
  * Type tags: `AUTH_INVALID`, `RATE_LIMIT`, `QUOTA_EXHAUSTED`,
13
17
  * `CONTEXT_OVERFLOW`, `CONTENT_BLOCKED`, `PROVIDER_UNAVAILABLE`,
@@ -25,6 +29,27 @@ const DETAIL_CAP = 500
25
29
  * @param {any} err
26
30
  * @returns {string | undefined}
27
31
  */
32
+ /**
33
+ * Replace verbatim occurrences of `key` in `detail` with a masked
34
+ * form. Long keys (≥ 16 chars) become `<first4>…<last4>` so a caller
35
+ * with multiple keys can still tell them apart from the masked
36
+ * substring; shorter keys fall back to `<redacted>` since revealing
37
+ * 8 chars would leak too much. Keys under 8 chars are treated as
38
+ * not-a-key (no scrub) — guards against pathological replacements
39
+ * on empty or fixture values.
40
+ * @param {string | undefined} detail
41
+ * @param {string | undefined} key
42
+ * @returns {string | undefined}
43
+ */
44
+ function scrubKey (detail, key) {
45
+ if (!detail || !key || typeof key !== 'string' || key.length < 8) return detail
46
+ if (!detail.includes(key)) return detail
47
+ const mask = key.length >= 16
48
+ ? `${key.slice(0, 4)}…${key.slice(-4)}`
49
+ : '<redacted>'
50
+ return detail.split(key).join(mask)
51
+ }
52
+
28
53
  function extractDetail (err) {
29
54
  if (!err) return undefined
30
55
  const nested = err.error?.message || err.response?.data?.error?.message
@@ -79,14 +104,20 @@ function matchesContextOverflow (msg) {
79
104
 
80
105
  /**
81
106
  * @param {unknown} e
107
+ * @param {string} [key] Optional API key the call was made with. When
108
+ * provided, any verbatim occurrence is replaced
109
+ * with `<redacted>` in the returned detail —
110
+ * providers occasionally echo the rejected key
111
+ * in error bodies (notably 401/403) and that
112
+ * must not leak.
82
113
  * @returns {import('#core/errors.js').TypedError}
83
114
  */
84
- export function classifyProviderError (e) {
115
+ export function classifyProviderError (e, key) {
85
116
  const err = /** @type {any} */(e)
86
117
  const status = err?.status
87
118
  const code = extractCode(err)
88
119
  const message = err?.message || ''
89
- const detail = extractDetail(err)
120
+ const detail = scrubKey(extractDetail(err), key)
90
121
 
91
122
  // --- Code-driven classification (runs before status buckets so
92
123
  // specific tags survive even when the upstream HTTP status is
@@ -142,12 +173,12 @@ export function classifyProviderError (e) {
142
173
  // --- Status-driven fallback. ---
143
174
 
144
175
  if (status === 401 || status === 403) {
145
- // Deliberately no detail — 401/403 bodies can echo the key.
146
176
  return {
147
177
  message: 'authentication failed',
148
178
  severity: 'error',
149
179
  retryable: false,
150
- type: 'AUTH_INVALID'
180
+ type: 'AUTH_INVALID',
181
+ detail
151
182
  }
152
183
  }
153
184
  if (status === 429) {
@@ -96,7 +96,7 @@ export async function * anthropic (envelope, deps = {}) {
96
96
  if (blocks.length) injectImageBlocks(conversation, blocks)
97
97
  } catch (e) {
98
98
  log?.warn({ err: e }, '[mohdel:anthropic] image load failed')
99
- yield { type: 'error', error: classifyProviderError(e) }
99
+ yield { type: 'error', error: classifyProviderError(e, envelope.auth?.key) }
100
100
  return
101
101
  }
102
102
  }
@@ -189,7 +189,7 @@ export async function * anthropic (envelope, deps = {}) {
189
189
  return
190
190
  }
191
191
  log?.warn({ err: e }, '[mohdel:anthropic] stream failed')
192
- yield { type: 'error', error: classifyProviderError(e) }
192
+ yield { type: 'error', error: classifyProviderError(e, envelope.auth?.key) }
193
193
  return
194
194
  }
195
195
 
@@ -58,7 +58,7 @@ export async function * gemini (envelope, deps = {}) {
58
58
  if (parts.length) injectParts(contents, parts)
59
59
  } catch (e) {
60
60
  log?.warn({ err: e }, '[mohdel:gemini] image load failed')
61
- yield { type: 'error', error: classifyProviderError(e) }
61
+ yield { type: 'error', error: classifyProviderError(e, envelope.auth?.key) }
62
62
  return
63
63
  }
64
64
  }
@@ -80,7 +80,7 @@ export async function * gemini (envelope, deps = {}) {
80
80
  // `typed` lets _videos.js surface PROVIDER_UNAVAILABLE on
81
81
  // upload-deadline timeouts; fall back to generic classification.
82
82
  const typed = /** @type {any} */(e).typed
83
- yield { type: 'error', error: typed || classifyProviderError(e) }
83
+ yield { type: 'error', error: typed || classifyProviderError(e, envelope.auth?.key) }
84
84
  return
85
85
  }
86
86
  }
@@ -172,7 +172,7 @@ export async function * gemini (envelope, deps = {}) {
172
172
  return
173
173
  }
174
174
  log?.warn({ err: e }, '[mohdel:gemini] stream failed')
175
- yield { type: 'error', error: classifyProviderError(e) }
175
+ yield { type: 'error', error: classifyProviderError(e, envelope.auth?.key) }
176
176
  return
177
177
  }
178
178
 
@@ -83,7 +83,7 @@ async function post (fetchFn, url, body, apiKey) {
83
83
  body: JSON.stringify(body)
84
84
  })
85
85
  } catch (e) {
86
- throw typedError(classifyProviderError(e).message, 'NET_ERROR', true)
86
+ throw typedError(classifyProviderError(e, apiKey).message, 'NET_ERROR', true)
87
87
  }
88
88
  if (!res.ok) {
89
89
  const text = await res.text().catch(() => '')
@@ -30,8 +30,8 @@ export async function openaiImage (envelope, deps = {}) {
30
30
  try {
31
31
  response = await client.images.generate(args)
32
32
  } catch (e) {
33
- throw Object.assign(new Error(classifyProviderError(e).message), {
34
- typed: classifyProviderError(e)
33
+ throw Object.assign(new Error(classifyProviderError(e, envelope.auth?.key).message), {
34
+ typed: classifyProviderError(e, envelope.auth?.key)
35
35
  })
36
36
  }
37
37
 
@@ -60,7 +60,7 @@ export async function * openai (envelope, deps = {}) {
60
60
  if (parts.length) injectImageParts(input, parts)
61
61
  } catch (e) {
62
62
  log?.warn({ err: e }, '[mohdel:openai] image load failed')
63
- yield { type: 'error', error: classifyProviderError(e) }
63
+ yield { type: 'error', error: classifyProviderError(e, envelope.auth?.key) }
64
64
  return
65
65
  }
66
66
  }
@@ -156,7 +156,7 @@ export async function * openai (envelope, deps = {}) {
156
156
  return
157
157
  }
158
158
  log?.warn({ err: e }, '[mohdel:openai] stream failed')
159
- yield { type: 'error', error: classifyProviderError(e) }
159
+ yield { type: 'error', error: classifyProviderError(e, envelope.auth?.key) }
160
160
  return
161
161
  }
162
162
 
@@ -51,7 +51,7 @@ export async function runImage (envelope, { resolveAdapter = getImageAdapter, sp
51
51
  const result = await adapter(envelope, spec ? { spec } : {})
52
52
  return { ok: true, result }
53
53
  } catch (e) {
54
- const typed = /** @type {any} */(e).typed || classifyProviderError(e)
54
+ const typed = /** @type {any} */(e).typed || classifyProviderError(e, envelope.auth?.key)
55
55
  return { ok: false, error: typed }
56
56
  }
57
57
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mohdel",
3
- "version": "0.98.1",
3
+ "version": "0.99.0",
4
4
  "license": "MIT",
5
5
  "author": {
6
6
  "name": "Christophe Le Bars",
@@ -83,16 +83,16 @@
83
83
  }
84
84
  },
85
85
  "optionalDependencies": {
86
- "@clack/prompts": "^1.2.0",
87
- "@opentelemetry/exporter-trace-otlp-grpc": "^0.215.0",
88
- "@opentelemetry/sdk-node": "^0.215.0",
86
+ "@clack/prompts": "^1.3.0",
87
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.216.0",
88
+ "@opentelemetry/sdk-node": "^0.216.0",
89
89
  "chalk": "^5.4.0",
90
- "mohdel-thin-gate-linux-x64-gnu": "0.98.1"
90
+ "mohdel-thin-gate-linux-x64-gnu": "0.99.0"
91
91
  },
92
92
  "dependencies": {
93
93
  "@anthropic-ai/sdk": "^0.91.1",
94
94
  "@cerebras/cerebras_cloud_sdk": "^1.61.1",
95
- "@google/genai": "^1.50.1",
95
+ "@google/genai": "^1.51.0",
96
96
  "@opentelemetry/api": "^1.9.1",
97
97
  "env-paths": "^4.0.0",
98
98
  "groq-sdk": "^1.1.2",