mohdel 0.114.1 → 0.116.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.
@@ -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 a SUBSET of input_tokens. Convert to
181
- // mohdel's additive convention (cacheReadInputTokens is separate from
182
- // inputTokens) by subtracting the cached portion before pricing. Both
183
- // adapters and computeCost stay simpler with the additive shape.
184
- const regularInputTokens = Math.max(0, inputTokens - cachedInputTokens)
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
  }
@@ -12,8 +12,6 @@
12
12
  * @module session/driver
13
13
  */
14
14
 
15
- import readline from 'node:readline'
16
-
17
15
  import { run } from './run.js'
18
16
  import { runImage } from './run_image.js'
19
17
  import { runTranscription } from './run_transcription.js'
@@ -29,8 +27,6 @@ const PRECANCEL_CAP = 128
29
27
  * @returns {Promise<void>}
30
28
  */
31
29
  export async function drive (stdin, stdout) {
32
- const rl = readline.createInterface({ input: stdin, crlfDelay: Infinity })
33
-
34
30
  /** @type {{callId: string, controller: AbortController} | null} */
35
31
  let currentCall = null
36
32
  /** @type {import('#core/envelope.js').CallEnvelope[]} */
@@ -38,6 +34,7 @@ export async function drive (stdin, stdout) {
38
34
  /** @type {(() => void) | null} */
39
35
  let queueNotify = null
40
36
  let stdinClosed = false
37
+ let framingError = null
41
38
  /** Cancel messages received before their envelope was dequeued.
42
39
  * JS Sets are insertion-ordered, so `values().next()` is the
43
40
  * oldest entry — cheap FIFO eviction at cap. */
@@ -51,7 +48,8 @@ export async function drive (stdin, stdout) {
51
48
  precancelled.add(callId)
52
49
  }
53
50
 
54
- rl.on('line', (line) => {
51
+ const onLine = (line) => {
52
+ if (framingError) return
55
53
  const trimmed = line.trim()
56
54
  if (!trimmed) return
57
55
 
@@ -59,7 +57,17 @@ export async function drive (stdin, stdout) {
59
57
  try {
60
58
  obj = JSON.parse(trimmed)
61
59
  } catch (e) {
62
- process.stderr.write(`session: failed to parse stdin line: ${e.message}\n`)
60
+ process.stderr.write(`session: malformed stdin line, exiting: ${e.message}\n`)
61
+ const error = {
62
+ message: 'SESSION_STDIN_MALFORMED',
63
+ detail: `stdin line is not valid JSON: ${e.message}`,
64
+ severity: 'error',
65
+ retryable: false,
66
+ type: 'SESSION_STDIN_MALFORMED'
67
+ }
68
+ stdout.write(JSON.stringify({ type: 'error', error }) + '\n')
69
+ framingError = Object.assign(new Error(error.message), { detail: error.detail })
70
+ if (queueNotify) { queueNotify(); queueNotify = null }
63
71
  return
64
72
  }
65
73
 
@@ -113,22 +121,35 @@ export async function drive (stdin, stdout) {
113
121
 
114
122
  envelopeQueue.push(obj)
115
123
  if (queueNotify) { queueNotify(); queueNotify = null }
116
- })
124
+ }
117
125
 
118
- rl.on('close', () => {
126
+ let stdinBuf = ''
127
+ stdin.setEncoding('utf8')
128
+ stdin.on('data', (chunk) => {
129
+ stdinBuf += chunk
130
+ let nl
131
+ while ((nl = stdinBuf.indexOf('\n')) !== -1) {
132
+ const line = stdinBuf.slice(0, nl)
133
+ stdinBuf = stdinBuf.slice(nl + 1)
134
+ onLine(line)
135
+ }
136
+ })
137
+ stdin.on('end', () => {
138
+ if (stdinBuf) onLine(stdinBuf)
119
139
  stdinClosed = true
120
140
  if (queueNotify) { queueNotify(); queueNotify = null }
121
141
  })
122
142
 
123
143
  while (true) {
124
- // `stdinClosed` flips asynchronously inside the `rl.on('close')`
144
+ // `stdinClosed` flips asynchronously inside the `stdin.on('end')`
125
145
  // callback above, which also signals `queueNotify`. The linter's
126
146
  // no-unmodified-loop-condition rule can't see callback mutation,
127
147
  // so it false-positives here.
128
148
  // eslint-disable-next-line no-unmodified-loop-condition
129
- while (envelopeQueue.length === 0 && !stdinClosed) {
149
+ while (envelopeQueue.length === 0 && !stdinClosed && !framingError) {
130
150
  await new Promise(resolve => { queueNotify = resolve })
131
151
  }
152
+ if (framingError) throw framingError
132
153
  if (envelopeQueue.length === 0) return
133
154
 
134
155
  const envelope = envelopeQueue.shift()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mohdel",
3
- "version": "0.114.1",
3
+ "version": "0.116.1",
4
4
  "license": "MIT",
5
5
  "author": {
6
6
  "name": "Christophe Le Bars",
@@ -83,20 +83,20 @@
83
83
  }
84
84
  },
85
85
  "optionalDependencies": {
86
- "@clack/prompts": "^1.5.1",
87
- "@opentelemetry/exporter-trace-otlp-grpc": "^0.219.0",
88
- "@opentelemetry/sdk-node": "^0.219.0",
86
+ "@clack/prompts": "^1.7.0",
87
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.220.0",
88
+ "@opentelemetry/sdk-node": "^0.220.0",
89
89
  "chalk": "^5.4.0",
90
- "mohdel-thin-gate-linux-x64-gnu": "0.114.1"
90
+ "mohdel-thin-gate-linux-x64-gnu": "0.116.1"
91
91
  },
92
92
  "dependencies": {
93
- "@anthropic-ai/sdk": "^0.105.0",
93
+ "@anthropic-ai/sdk": "^0.110.0",
94
94
  "@cerebras/cerebras_cloud_sdk": "^1.61.1",
95
- "@google/genai": "^2.9.0",
95
+ "@google/genai": "^2.11.0",
96
96
  "@opentelemetry/api": "^1.9.1",
97
97
  "env-paths": "^4.0.0",
98
- "groq-sdk": "^1.2.1",
99
- "openai": "^6.44.0",
98
+ "groq-sdk": "^1.3.0",
99
+ "openai": "^6.46.0",
100
100
  "undici": "^7.24.5"
101
101
  },
102
102
  "lint-staged": {
@@ -104,9 +104,9 @@
104
104
  },
105
105
  "devDependencies": {
106
106
  "gpt-tokenizer": "^3.4.0",
107
- "lint-staged": "^17.0.7",
108
- "release-it": "^20.2.0",
107
+ "lint-staged": "^17.0.8",
108
+ "release-it": "^20.2.1",
109
109
  "standard": "^17.1.2",
110
- "vitest": "^4.1.9"
110
+ "vitest": "^4.1.10"
111
111
  }
112
112
  }