mohdel 0.115.0 → 0.117.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/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 */
|
|
@@ -77,6 +77,7 @@ export async function * openai (envelope, deps = {}) {
|
|
|
77
77
|
let outputTokens = 0
|
|
78
78
|
let thinkingTokens = 0
|
|
79
79
|
let cachedInputTokens = 0
|
|
80
|
+
let cacheWriteTokens = 0
|
|
80
81
|
let status = STATUS_COMPLETED
|
|
81
82
|
/** @type {string | undefined} */
|
|
82
83
|
let warning
|
|
@@ -132,6 +133,7 @@ export async function * openai (envelope, deps = {}) {
|
|
|
132
133
|
outputTokens = event.response.usage.output_tokens ?? 0
|
|
133
134
|
thinkingTokens = event.response.usage.output_tokens_details?.reasoning_tokens ?? 0
|
|
134
135
|
cachedInputTokens = event.response.usage.input_tokens_details?.cached_tokens ?? 0
|
|
136
|
+
cacheWriteTokens = event.response.usage.input_tokens_details?.cache_write_tokens ?? 0
|
|
135
137
|
}
|
|
136
138
|
if (toolItems.size > 0) {
|
|
137
139
|
status = STATUS_TOOL_USE
|
|
@@ -148,6 +150,7 @@ export async function * openai (envelope, deps = {}) {
|
|
|
148
150
|
outputTokens = event.response.usage.output_tokens ?? 0
|
|
149
151
|
thinkingTokens = event.response.usage.output_tokens_details?.reasoning_tokens ?? 0
|
|
150
152
|
cachedInputTokens = event.response.usage.input_tokens_details?.cached_tokens ?? 0
|
|
153
|
+
cacheWriteTokens = event.response.usage.input_tokens_details?.cache_write_tokens ?? 0
|
|
151
154
|
}
|
|
152
155
|
break
|
|
153
156
|
|
|
@@ -177,11 +180,12 @@ export async function * openai (envelope, deps = {}) {
|
|
|
177
180
|
// one from the other for the message-only count.
|
|
178
181
|
const messageOutputTokens = Math.max(0, outputTokens - thinkingTokens)
|
|
179
182
|
|
|
180
|
-
// OpenAI counts cached_tokens as
|
|
181
|
-
// mohdel's additive convention (
|
|
182
|
-
// inputTokens) by subtracting
|
|
183
|
-
//
|
|
184
|
-
|
|
183
|
+
// OpenAI counts cached_tokens and cache_write_tokens as SUBSETS of
|
|
184
|
+
// input_tokens. Convert to mohdel's additive convention (cacheRead/
|
|
185
|
+
// cacheWriteInputTokens are separate from inputTokens) by subtracting
|
|
186
|
+
// both portions before pricing. Both adapters and computeCost stay
|
|
187
|
+
// simpler with the additive shape.
|
|
188
|
+
const regularInputTokens = Math.max(0, inputTokens - cachedInputTokens - cacheWriteTokens)
|
|
185
189
|
|
|
186
190
|
/** @type {import('#core/events.js').DoneEvent} */
|
|
187
191
|
const done = {
|
|
@@ -192,6 +196,7 @@ export async function * openai (envelope, deps = {}) {
|
|
|
192
196
|
inputTokens: regularInputTokens,
|
|
193
197
|
outputTokens: messageOutputTokens,
|
|
194
198
|
thinkingTokens,
|
|
199
|
+
...(cacheWriteTokens > 0 && { cacheWriteInputTokens: cacheWriteTokens }),
|
|
195
200
|
...(cachedInputTokens > 0 && { cacheReadInputTokens: cachedInputTokens }),
|
|
196
201
|
cost: costFor(
|
|
197
202
|
catalogKey(envelope.model),
|
|
@@ -199,6 +204,7 @@ export async function * openai (envelope, deps = {}) {
|
|
|
199
204
|
inputTokens: regularInputTokens,
|
|
200
205
|
outputTokens: messageOutputTokens,
|
|
201
206
|
thinkingTokens,
|
|
207
|
+
cacheWriteInputTokens: cacheWriteTokens,
|
|
202
208
|
cacheReadInputTokens: cachedInputTokens
|
|
203
209
|
}
|
|
204
210
|
),
|
|
@@ -273,6 +279,7 @@ function buildRequest (envelope, input, instructions) {
|
|
|
273
279
|
if (envelope.identifier) {
|
|
274
280
|
if (provider === 'openai') {
|
|
275
281
|
request.safety_identifier = envelope.identifier
|
|
282
|
+
request.prompt_cache_key = envelope.identifier
|
|
276
283
|
} else {
|
|
277
284
|
request.user = envelope.identifier
|
|
278
285
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mohdel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.117.0",
|
|
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.0"
|
|
91
91
|
},
|
|
92
92
|
"dependencies": {
|
|
93
93
|
"@anthropic-ai/sdk": "^0.110.0",
|
|
94
94
|
"@cerebras/cerebras_cloud_sdk": "^1.61.1",
|
|
95
|
-
"@google/genai": "^2.
|
|
95
|
+
"@google/genai": "^2.11.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.46.0",
|
|
100
100
|
"undici": "^7.24.5"
|
|
101
101
|
},
|
|
102
102
|
"lint-staged": {
|