mohdel 0.116.1 → 0.117.1
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/core/envelope.js
CHANGED
|
@@ -113,6 +113,11 @@
|
|
|
113
113
|
* @typedef {object} MessagePart
|
|
114
114
|
* @property {('text'|'reasoning')} type
|
|
115
115
|
* @property {string} text
|
|
116
|
+
* @property {('5m'|'1h')} [cache]
|
|
117
|
+
* Prompt-cache marker. On system parts: a breakpoint at this block.
|
|
118
|
+
* On non-system parts: opts the whole conversation into prefix
|
|
119
|
+
* caching (adapter places the breakpoints). Providers with
|
|
120
|
+
* automatic caching ignore it.
|
|
116
121
|
*/
|
|
117
122
|
|
|
118
123
|
/**
|
|
@@ -90,7 +90,7 @@ export async function * anthropic (envelope, deps = {}) {
|
|
|
90
90
|
const start = String(process.hrtime.bigint())
|
|
91
91
|
let first = null
|
|
92
92
|
|
|
93
|
-
const { system, conversation } = splitPrompt(envelope.prompt)
|
|
93
|
+
const { system, conversation, conversationCacheTtl } = splitPrompt(envelope.prompt)
|
|
94
94
|
|
|
95
95
|
// Attach images to the last user message before building the request.
|
|
96
96
|
if (envelope.images?.length) {
|
|
@@ -105,7 +105,7 @@ export async function * anthropic (envelope, deps = {}) {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
const request = buildRequest(envelope, conversation, system)
|
|
108
|
+
const request = buildRequest(envelope, conversation, system, conversationCacheTtl)
|
|
109
109
|
|
|
110
110
|
// F53: accumulate via array + join to avoid per-delta V8 cons-string
|
|
111
111
|
// churn. Materialized at each exit point.
|
|
@@ -303,8 +303,11 @@ function safeParseJson (s) {
|
|
|
303
303
|
* @param {string | Array<{type: string, text: string, cache_control?: object}>} system
|
|
304
304
|
* Either a flat string (legacy) or an array of typed blocks with optional
|
|
305
305
|
* `cache_control` for prompt caching. The Anthropic SDK accepts both shapes.
|
|
306
|
+
* @param {'5m'|'1h'|null} [conversationCacheTtl]
|
|
307
|
+
* When set, place conversation-prefix breakpoints (see
|
|
308
|
+
* `applyCacheBreakpoints`).
|
|
306
309
|
*/
|
|
307
|
-
function buildRequest (envelope, conversation, system) {
|
|
310
|
+
function buildRequest (envelope, conversation, system, conversationCacheTtl = null) {
|
|
308
311
|
const spec = getSpec(catalogKey(envelope.model))
|
|
309
312
|
const outputTokenLimit = spec?.outputTokenLimit
|
|
310
313
|
|
|
@@ -347,22 +350,106 @@ function buildRequest (envelope, conversation, system) {
|
|
|
347
350
|
}
|
|
348
351
|
}
|
|
349
352
|
|
|
353
|
+
applyCacheBreakpoints(request, conversationCacheTtl)
|
|
354
|
+
|
|
350
355
|
return request
|
|
351
356
|
}
|
|
352
357
|
|
|
358
|
+
/** Anthropic caps `cache_control` breakpoints at 4 per request. */
|
|
359
|
+
const MAX_CACHE_BREAKPOINTS = 4
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Conversation breakpoints sit at stable block positions (indices
|
|
363
|
+
* ≡ 15 mod 16) so that consecutive requests share entry positions
|
|
364
|
+
* even when one request appends more blocks than Anthropic's
|
|
365
|
+
* 20-block cache lookback covers.
|
|
366
|
+
*/
|
|
367
|
+
const MILESTONE_INTERVAL = 16
|
|
368
|
+
|
|
369
|
+
/** @param {'5m'|'1h'} ttl */
|
|
370
|
+
function cacheControlFor (ttl) {
|
|
371
|
+
return ttl === '1h'
|
|
372
|
+
? { type: 'ephemeral', ttl: '1h' }
|
|
373
|
+
: { type: 'ephemeral' }
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* A `cache: '5m'|'1h'` marker on any non-system message part opts the
|
|
378
|
+
* conversation into prefix caching. Over-cap system breakpoints are
|
|
379
|
+
* dropped middle-first: the first one is the most stable read fallback
|
|
380
|
+
* (volatile blocks like session memory sit later), the last covers the
|
|
381
|
+
* full system prompt. Dropping a breakpoint never changes prompt
|
|
382
|
+
* content — only cache eligibility.
|
|
383
|
+
*
|
|
384
|
+
* @param {{system?: any, messages: Array<{role: string, content: any}>}} request
|
|
385
|
+
* @param {'5m'|'1h'|null} conversationTtl
|
|
386
|
+
*/
|
|
387
|
+
function applyCacheBreakpoints (request, conversationTtl) {
|
|
388
|
+
let messageBreakpoints = 0
|
|
389
|
+
if (conversationTtl && request.messages.length) {
|
|
390
|
+
messageBreakpoints = placeConversationBreakpoints(request.messages, conversationTtl)
|
|
391
|
+
}
|
|
392
|
+
if (Array.isArray(request.system)) {
|
|
393
|
+
const marked = request.system.filter(b => b.cache_control)
|
|
394
|
+
const budget = MAX_CACHE_BREAKPOINTS - messageBreakpoints
|
|
395
|
+
while (marked.length > budget && marked.length > 0) {
|
|
396
|
+
const drop = marked.length > 1 ? marked.splice(1, 1)[0] : marked.shift()
|
|
397
|
+
delete drop.cache_control
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* @param {Array<{role: string, content: any}>} messages
|
|
404
|
+
* @param {'5m'|'1h'} ttl
|
|
405
|
+
* @returns {number} breakpoints placed
|
|
406
|
+
*/
|
|
407
|
+
function placeConversationBreakpoints (messages, ttl) {
|
|
408
|
+
const blockCount = messages.reduce(
|
|
409
|
+
(n, m) => n + (Array.isArray(m.content) ? m.content.length : 1), 0)
|
|
410
|
+
if (blockCount === 0) return 0
|
|
411
|
+
const trailing = blockCount - 1
|
|
412
|
+
const targets = [trailing]
|
|
413
|
+
const milestone = MILESTONE_INTERVAL * Math.floor(trailing / MILESTONE_INTERVAL) - 1
|
|
414
|
+
if (milestone >= 0 && milestone !== trailing) targets.unshift(milestone)
|
|
415
|
+
let placed = 0
|
|
416
|
+
let idx = 0
|
|
417
|
+
for (const m of messages) {
|
|
418
|
+
const len = Array.isArray(m.content) ? m.content.length : 1
|
|
419
|
+
for (const t of targets) {
|
|
420
|
+
if (t >= idx && t < idx + len) {
|
|
421
|
+
if (!Array.isArray(m.content)) {
|
|
422
|
+
m.content = [{ type: 'text', text: m.content }]
|
|
423
|
+
}
|
|
424
|
+
m.content[t - idx].cache_control = cacheControlFor(ttl)
|
|
425
|
+
placed++
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
idx += len
|
|
429
|
+
}
|
|
430
|
+
return placed
|
|
431
|
+
}
|
|
432
|
+
|
|
353
433
|
/** @param {string | import('#core/envelope.js').Message[]} prompt */
|
|
354
434
|
function splitPrompt (prompt) {
|
|
355
435
|
if (typeof prompt === 'string') {
|
|
356
|
-
return { system: '', conversation: [{ role: 'user', content: prompt }] }
|
|
436
|
+
return { system: '', conversation: [{ role: 'user', content: prompt }], conversationCacheTtl: null }
|
|
357
437
|
}
|
|
358
438
|
/** @type {string[]} */
|
|
359
439
|
const systemParts = []
|
|
360
440
|
/** @type {Array<{type: string, text: string, cache_control?: object}>} */
|
|
361
441
|
const systemBlocks = []
|
|
362
442
|
let hasCacheMarkers = false
|
|
443
|
+
/** @type {'5m'|'1h'|null} */
|
|
444
|
+
let conversationCacheTtl = null
|
|
363
445
|
/** @type {Array<{role: string, content: any}>} */
|
|
364
446
|
const conversation = []
|
|
365
447
|
for (const m of prompt) {
|
|
448
|
+
if (m.role !== 'system' && Array.isArray(m.content)) {
|
|
449
|
+
for (const p of m.content) {
|
|
450
|
+
if (p?.cache === '5m' || p?.cache === '1h') conversationCacheTtl = p.cache
|
|
451
|
+
}
|
|
452
|
+
}
|
|
366
453
|
if (m.role === 'system') {
|
|
367
454
|
// Translate spore-style cache markers ({text, cache: '5m'|'1h'}) into
|
|
368
455
|
// Anthropic's cache_control. Preserves the block boundary that spore
|
|
@@ -429,7 +516,7 @@ function splitPrompt (prompt) {
|
|
|
429
516
|
const system = hasCacheMarkers
|
|
430
517
|
? systemBlocks
|
|
431
518
|
: systemParts.filter(Boolean).join('\n\n')
|
|
432
|
-
return { system, conversation }
|
|
519
|
+
return { system, conversation, conversationCacheTtl }
|
|
433
520
|
}
|
|
434
521
|
|
|
435
522
|
/** @param {string | import('#core/envelope.js').MessagePart[]} content */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mohdel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.117.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Christophe Le Bars",
|
|
@@ -87,16 +87,16 @@
|
|
|
87
87
|
"@opentelemetry/exporter-trace-otlp-grpc": "^0.220.0",
|
|
88
88
|
"@opentelemetry/sdk-node": "^0.220.0",
|
|
89
89
|
"chalk": "^5.4.0",
|
|
90
|
-
"mohdel-thin-gate-linux-x64-gnu": "0.
|
|
90
|
+
"mohdel-thin-gate-linux-x64-gnu": "0.117.1"
|
|
91
91
|
},
|
|
92
92
|
"dependencies": {
|
|
93
|
-
"@anthropic-ai/sdk": "^0.
|
|
94
|
-
"@cerebras/cerebras_cloud_sdk": "^1.
|
|
95
|
-
"@google/genai": "^2.
|
|
93
|
+
"@anthropic-ai/sdk": "^0.112.2",
|
|
94
|
+
"@cerebras/cerebras_cloud_sdk": "^1.91.0",
|
|
95
|
+
"@google/genai": "^2.12.0",
|
|
96
96
|
"@opentelemetry/api": "^1.9.1",
|
|
97
97
|
"env-paths": "^4.0.0",
|
|
98
98
|
"groq-sdk": "^1.3.0",
|
|
99
|
-
"openai": "^6.
|
|
99
|
+
"openai": "^6.48.0",
|
|
100
100
|
"undici": "^7.24.5"
|
|
101
101
|
},
|
|
102
102
|
"lint-staged": {
|