@mlx-node/server 0.0.13 → 0.0.15

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.
Files changed (45) hide show
  1. package/dist/host/discover.d.ts +3 -6
  2. package/dist/host/discover.d.ts.map +1 -1
  3. package/dist/host/discover.js +9 -42
  4. package/dist/host/index.d.ts +2 -2
  5. package/dist/host/index.d.ts.map +1 -1
  6. package/dist/host/index.js +8 -1
  7. package/package.json +9 -4
  8. package/src/auth.ts +111 -0
  9. package/src/chat-session-warm-reuse.ts +96 -0
  10. package/src/endpoints/messages-count-tokens.ts +164 -0
  11. package/src/endpoints/messages.ts +1802 -0
  12. package/src/endpoints/models.ts +20 -0
  13. package/src/endpoints/responses.ts +3928 -0
  14. package/src/errors.ts +120 -0
  15. package/src/handler.ts +195 -0
  16. package/src/health.ts +213 -0
  17. package/src/host/discover.ts +25 -0
  18. package/src/host/env-policy.ts +81 -0
  19. package/src/host/index.ts +496 -0
  20. package/src/host/logger.ts +419 -0
  21. package/src/host/net.ts +100 -0
  22. package/src/host/paths.ts +77 -0
  23. package/src/host/swap.ts +200 -0
  24. package/src/host/temp-root.ts +110 -0
  25. package/src/idle-sweeper.ts +555 -0
  26. package/src/index.ts +114 -0
  27. package/src/load-model.ts +92 -0
  28. package/src/mappers/anthropic-request.ts +485 -0
  29. package/src/mappers/anthropic-response.ts +306 -0
  30. package/src/mappers/request.ts +456 -0
  31. package/src/mappers/response.ts +163 -0
  32. package/src/model-work-coordinator.ts +416 -0
  33. package/src/pending-writes.ts +481 -0
  34. package/src/registry.ts +691 -0
  35. package/src/router.ts +220 -0
  36. package/src/server.ts +579 -0
  37. package/src/session-registry.ts +1371 -0
  38. package/src/stop-sequence-buffer.ts +161 -0
  39. package/src/streaming.ts +205 -0
  40. package/src/text-recovery.ts +41 -0
  41. package/src/timing.ts +236 -0
  42. package/src/tool-call-buffer.ts +78 -0
  43. package/src/transport-visibility.ts +185 -0
  44. package/src/types-anthropic.ts +409 -0
  45. package/src/types.ts +470 -0
@@ -0,0 +1,306 @@
1
+ /** ChatResult / ChatStreamEvent → Anthropic Messages API output. */
2
+
3
+ import type { ChatResult } from '@mlx-node/core';
4
+
5
+ import { mergeTimingUsageExtensions, type PerformanceMetricsForUsage, type ServerTimingForUsage } from '../timing.js';
6
+ import type {
7
+ AnthropicContentBlockDeltaEvent,
8
+ AnthropicContentBlockStartEvent,
9
+ AnthropicContentBlockStopEvent,
10
+ AnthropicDelta,
11
+ AnthropicMessageDeltaEvent,
12
+ AnthropicMessageStartEvent,
13
+ AnthropicMessageStopEvent,
14
+ AnthropicMessagesRequest,
15
+ AnthropicMessagesResponse,
16
+ AnthropicResponseContent,
17
+ } from '../types-anthropic.js';
18
+ import { genId } from './response.js';
19
+
20
+ function parseArguments(args: Record<string, unknown> | string): Record<string, unknown> {
21
+ if (typeof args === 'string') {
22
+ return JSON.parse(args) as Record<string, unknown>;
23
+ }
24
+ return args;
25
+ }
26
+
27
+ /**
28
+ * Translate a native tool-call id (minted as `call_<uuid>` by the Rust
29
+ * parser, which keeps the OpenAI Responses convention) to the Anthropic
30
+ * Messages wire convention (`toolu_<uuid>`). The uuid body is preserved
31
+ * verbatim so the inverse `anthropicToolUseIdToInternal` round-trips
32
+ * losslessly when clients echo the id back via `tool_result.tool_use_id`.
33
+ *
34
+ * Defensive: ids that do not have the expected `call_` prefix (e.g. a
35
+ * legacy caller, an in-process driver constructing its own id, or a
36
+ * future variant) pass through unchanged so this function never
37
+ * synthesizes a wrong id.
38
+ */
39
+ export function internalToolCallIdToAnthropic(id: string): string {
40
+ return id.startsWith('call_') ? `toolu_${id.slice('call_'.length)}` : id;
41
+ }
42
+
43
+ /**
44
+ * Inverse of `internalToolCallIdToAnthropic`: translate an incoming
45
+ * Anthropic `tool_use_id` (`toolu_<uuid>`) back to the internal `call_*`
46
+ * shape so historical assistant turns and the tool_call_id lookup in
47
+ * the native session store keep matching. Ids without the expected
48
+ * `toolu_` prefix pass through unchanged for the same defensive reason
49
+ * (some legacy callers send raw `call_*` directly).
50
+ */
51
+ export function anthropicToolUseIdToInternal(id: string): string {
52
+ return id.startsWith('toolu_') ? `call_${id.slice('toolu_'.length)}` : id;
53
+ }
54
+
55
+ export function mapStopReason(
56
+ finishReason: string,
57
+ hasToolCalls: boolean,
58
+ matchedStopSequence?: string | null,
59
+ ): 'end_turn' | 'max_tokens' | 'stop_sequence' | 'tool_use' {
60
+ if (matchedStopSequence) {
61
+ return 'stop_sequence';
62
+ }
63
+ if (finishReason === 'length') {
64
+ return 'max_tokens';
65
+ }
66
+ if (hasToolCalls) {
67
+ return 'tool_use';
68
+ }
69
+ return 'end_turn';
70
+ }
71
+
72
+ export function containsToolCallMarkup(rawText: string): boolean {
73
+ return (
74
+ rawText.includes('<tool_call') ||
75
+ rawText.includes('</tool_call') ||
76
+ rawText.includes('<|tool_call') ||
77
+ rawText.includes('<tool_call|>')
78
+ );
79
+ }
80
+
81
+ export function recoverSuppressedToolCallText(rawText: string): string {
82
+ return rawText
83
+ .replace(/<\|channel>[\s\S]*?(?:<channel\|>|$)/g, '')
84
+ .replace(/<channel\|>/g, '')
85
+ .replace(/<\|tool_call>[\s\S]*?(?:<tool_call\|>|$)/g, '')
86
+ .replace(/<tool_call>[\s\S]*?(?:<\/tool_call>|$)/g, '')
87
+ .replace(/<\|tool_response>[\s\S]*?(?:<tool_response\|>|$)/g, '')
88
+ .replace(/<\|tool>[\s\S]*?(?:<tool\|>|$)/g, '')
89
+ .replace(/<\|turn>[^\n]*(?:\n|$)/g, '')
90
+ .replace(/<turn\|>/g, '');
91
+ }
92
+
93
+ export function buildAnthropicContent(
94
+ result: ChatResult,
95
+ allowToolUse = true,
96
+ stopMatched = false,
97
+ ): AnthropicResponseContent[] {
98
+ const content: AnthropicResponseContent[] = [];
99
+
100
+ if (result.thinking) {
101
+ content.push({ type: 'thinking', thinking: result.thinking });
102
+ }
103
+
104
+ const parsedToolCalls = result.toolCalls.filter((t) => t.status === 'ok');
105
+ // A matched stop sequence halts generation at its position, so any tool call
106
+ // whose tag would have followed the stop boundary is dropped and the
107
+ // truncated visible text (`result.text`, already cut at the stop) is emitted
108
+ // verbatim — the suppressed-markup recovery is skipped because it would
109
+ // re-introduce text that lived after the stop.
110
+ const okToolCalls = stopMatched || !allowToolUse ? [] : parsedToolCalls;
111
+ const text =
112
+ !stopMatched &&
113
+ !allowToolUse &&
114
+ result.text.length === 0 &&
115
+ parsedToolCalls.length > 0 &&
116
+ containsToolCallMarkup(result.rawText)
117
+ ? recoverSuppressedToolCallText(result.rawText)
118
+ : result.text;
119
+
120
+ // Emit a text block unless tool calls exist and there is no text.
121
+ if (text || okToolCalls.length === 0) {
122
+ content.push({ type: 'text', text });
123
+ }
124
+
125
+ for (const tc of okToolCalls) {
126
+ // Translate native `call_<uuid>` to Anthropic `toolu_<uuid>` at the
127
+ // wire boundary. The fallback `genId('toolu_')` covers the case where
128
+ // the native parser did not mint an id (an in-process driver or a
129
+ // legacy bridge — present-day Rust paths always populate it).
130
+ content.push({
131
+ type: 'tool_use',
132
+ id: tc.id != null ? internalToolCallIdToAnthropic(tc.id) : genId('toolu_'),
133
+ name: tc.name,
134
+ input: parseArguments(tc.arguments),
135
+ });
136
+ }
137
+
138
+ return content;
139
+ }
140
+
141
+ export function buildAnthropicResponse(
142
+ result: ChatResult,
143
+ req: AnthropicMessagesRequest,
144
+ messageId: string,
145
+ performance?: PerformanceMetricsForUsage,
146
+ allowToolUse = true,
147
+ serverTiming?: ServerTimingForUsage,
148
+ matchedStopSequence?: string | null,
149
+ ): AnthropicMessagesResponse {
150
+ // A matched stop sequence takes precedence over tool_use: suppress the tool
151
+ // calls so `stop_reason: 'stop_sequence'` is never emitted alongside a
152
+ // tool_use block whose tag followed the stop boundary.
153
+ const stopMatched = Boolean(matchedStopSequence);
154
+ const okToolCalls = allowToolUse && !stopMatched ? result.toolCalls.filter((t) => t.status === 'ok') : [];
155
+ const hasToolCalls = okToolCalls.length > 0;
156
+
157
+ // Cache accounting (Anthropic Messages API spec):
158
+ // * On a cache HIT (`cachedTokens > 0`) the wire MUST emit
159
+ // `cache_read_input_tokens: cachedTokens` and reduce
160
+ // `input_tokens` to the unsuffixed remainder
161
+ // `promptTokens - cachedTokens` — Claude Code (and other
162
+ // Anthropic-compatible UIs) read this directly for cost /
163
+ // billing display, and a wire that left `input_tokens` at the
164
+ // full prompt count would silently double-bill the cached
165
+ // prefix.
166
+ // * On a cache MISS (`cachedTokens === 0`) the cache fields are
167
+ // OMITTED — they are optional in the spec and other
168
+ // Anthropic-compatible servers elide them on misses.
169
+ // * `cache_creation_input_tokens` stays unset: this server's KV
170
+ // reuse is implicit (no `cache_control` breakpoints), so a
171
+ // client that did not request explicit caching should never
172
+ // see a non-zero creation count.
173
+ const cachedTokens = result.cachedTokens;
174
+ const usage: AnthropicMessagesResponse['usage'] =
175
+ cachedTokens > 0
176
+ ? {
177
+ input_tokens: result.promptTokens - cachedTokens,
178
+ output_tokens: result.numTokens,
179
+ cache_read_input_tokens: cachedTokens,
180
+ }
181
+ : {
182
+ input_tokens: result.promptTokens,
183
+ output_tokens: result.numTokens,
184
+ };
185
+
186
+ // Server-extension perf fields. Same gating pattern as
187
+ // `cache_read_input_tokens`: only land on the wire when the native
188
+ // dispatch produced a finite, positive value — `undefined` /
189
+ // `NaN` / `0` is elided so the launcher's verbose log can read
190
+ // absence as "not plumbed" instead of treating zero as a real
191
+ // measurement. Cache-context fields make the prefill rate explicit:
192
+ // on cached-prefix turns the denominator is the uncached suffix, not
193
+ // the full logical prompt.
194
+ mergeTimingUsageExtensions(usage, performance, result.promptTokens, result.numTokens, cachedTokens, serverTiming);
195
+
196
+ return {
197
+ id: messageId,
198
+ type: 'message',
199
+ role: 'assistant',
200
+ model: req.model,
201
+ content: buildAnthropicContent(result, allowToolUse, stopMatched),
202
+ stop_reason: mapStopReason(result.finishReason, hasToolCalls, matchedStopSequence),
203
+ stop_sequence: matchedStopSequence ?? null,
204
+ usage,
205
+ };
206
+ }
207
+
208
+ // Streaming helpers
209
+
210
+ /** Embedded message has empty content and zero output_tokens at start. */
211
+ export function buildMessageStartEvent(
212
+ req: AnthropicMessagesRequest,
213
+ messageId: string,
214
+ inputTokens: number,
215
+ ): AnthropicMessageStartEvent {
216
+ return {
217
+ type: 'message_start',
218
+ message: {
219
+ id: messageId,
220
+ type: 'message',
221
+ role: 'assistant',
222
+ model: req.model,
223
+ content: [],
224
+ stop_reason: null,
225
+ stop_sequence: null,
226
+ usage: {
227
+ input_tokens: inputTokens,
228
+ output_tokens: 0,
229
+ },
230
+ },
231
+ };
232
+ }
233
+
234
+ export function buildContentBlockStart(
235
+ index: number,
236
+ block: AnthropicResponseContent,
237
+ ): AnthropicContentBlockStartEvent {
238
+ return {
239
+ type: 'content_block_start',
240
+ index,
241
+ content_block: block,
242
+ };
243
+ }
244
+
245
+ export function buildContentBlockDelta(index: number, delta: AnthropicDelta): AnthropicContentBlockDeltaEvent {
246
+ return {
247
+ type: 'content_block_delta',
248
+ index,
249
+ delta,
250
+ };
251
+ }
252
+
253
+ export function buildContentBlockStop(index: number): AnthropicContentBlockStopEvent {
254
+ return {
255
+ type: 'content_block_stop',
256
+ index,
257
+ };
258
+ }
259
+
260
+ export function buildMessageDelta(
261
+ stopReason: string,
262
+ outputTokens: number,
263
+ inputTokens?: number,
264
+ cachedTokens?: number,
265
+ performance?: PerformanceMetricsForUsage,
266
+ serverTiming?: ServerTimingForUsage,
267
+ stopSequence?: string | null,
268
+ ): AnthropicMessageDeltaEvent {
269
+ // Streaming `message_delta` mirrors the non-streaming response's
270
+ // cache accounting: when `cachedTokens > 0` we emit
271
+ // `cache_read_input_tokens: cachedTokens` AND subtract that count
272
+ // from `input_tokens`. On a cache miss (or when `cachedTokens` is
273
+ // omitted by an in-process driver / mock) the cache fields stay
274
+ // off the wire. See the matching block on `buildAnthropicResponse`
275
+ // and the field-level docstrings on `AnthropicUsage`.
276
+ const usage: AnthropicMessageDeltaEvent['usage'] = {
277
+ output_tokens: outputTokens,
278
+ };
279
+ if (cachedTokens != null && cachedTokens > 0) {
280
+ if (inputTokens != null) {
281
+ usage.input_tokens = inputTokens - cachedTokens;
282
+ }
283
+ usage.cache_read_input_tokens = cachedTokens;
284
+ } else if (inputTokens != null) {
285
+ usage.input_tokens = inputTokens;
286
+ }
287
+ // Server-extension perf fields — same gating pattern as the
288
+ // cache-field block above. See `buildAnthropicResponse` for the
289
+ // matching non-streaming branch and the docstring on
290
+ // `AnthropicUsage` for the wire-format rationale.
291
+ mergeTimingUsageExtensions(usage, performance, inputTokens, outputTokens, cachedTokens, serverTiming);
292
+ return {
293
+ type: 'message_delta',
294
+ delta: {
295
+ stop_reason: stopReason,
296
+ stop_sequence: stopSequence ?? null,
297
+ },
298
+ usage,
299
+ };
300
+ }
301
+
302
+ export function buildMessageStop(): AnthropicMessageStopEvent {
303
+ return {
304
+ type: 'message_stop',
305
+ };
306
+ }