mohdel 0.98.2 → 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.
- package/js/session/adapters/_chat_completions.js +3 -3
- package/js/session/adapters/_errors.js +39 -8
- package/js/session/adapters/anthropic.js +2 -2
- package/js/session/adapters/gemini.js +3 -3
- package/js/session/adapters/image/novita.js +1 -1
- package/js/session/adapters/image/openai.js +2 -2
- package/js/session/adapters/openai.js +2 -2
- package/js/session/run_image.js +1 -1
- package/package.json +2 -2
|
@@ -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.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
package/js/session/run_image.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.99.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.216.0",
|
|
88
88
|
"@opentelemetry/sdk-node": "^0.216.0",
|
|
89
89
|
"chalk": "^5.4.0",
|
|
90
|
-
"mohdel-thin-gate-linux-x64-gnu": "0.
|
|
90
|
+
"mohdel-thin-gate-linux-x64-gnu": "0.99.0"
|
|
91
91
|
},
|
|
92
92
|
"dependencies": {
|
|
93
93
|
"@anthropic-ai/sdk": "^0.91.1",
|