@mlx-node/server 0.0.0 → 0.0.8

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 (74) hide show
  1. package/dist/chat-session-warm-reuse.d.ts +51 -0
  2. package/dist/chat-session-warm-reuse.d.ts.map +1 -0
  3. package/dist/chat-session-warm-reuse.js +68 -0
  4. package/dist/endpoints/messages-count-tokens.d.ts +8 -0
  5. package/dist/endpoints/messages-count-tokens.d.ts.map +1 -0
  6. package/dist/endpoints/messages-count-tokens.js +121 -0
  7. package/dist/endpoints/messages.d.ts +57 -5
  8. package/dist/endpoints/messages.d.ts.map +1 -1
  9. package/dist/endpoints/messages.js +1043 -147
  10. package/dist/endpoints/models.d.ts +2 -1
  11. package/dist/endpoints/models.d.ts.map +1 -1
  12. package/dist/endpoints/models.js +2 -2
  13. package/dist/endpoints/responses.d.ts +20 -7
  14. package/dist/endpoints/responses.d.ts.map +1 -1
  15. package/dist/endpoints/responses.js +572 -82
  16. package/dist/errors.d.ts +1 -0
  17. package/dist/errors.d.ts.map +1 -1
  18. package/dist/errors.js +3 -0
  19. package/dist/handler.d.ts +42 -0
  20. package/dist/handler.d.ts.map +1 -1
  21. package/dist/handler.js +6 -1
  22. package/dist/idle-sweeper.d.ts +245 -0
  23. package/dist/idle-sweeper.d.ts.map +1 -0
  24. package/dist/idle-sweeper.js +408 -0
  25. package/dist/index.d.ts +8 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +10 -0
  28. package/dist/mappers/anthropic-request.d.ts +24 -2
  29. package/dist/mappers/anthropic-request.d.ts.map +1 -1
  30. package/dist/mappers/anthropic-request.js +222 -24
  31. package/dist/mappers/anthropic-response.d.ts +29 -4
  32. package/dist/mappers/anthropic-response.d.ts.map +1 -1
  33. package/dist/mappers/anthropic-response.js +143 -21
  34. package/dist/mappers/request.d.ts +48 -0
  35. package/dist/mappers/request.d.ts.map +1 -1
  36. package/dist/mappers/request.js +211 -35
  37. package/dist/mappers/response.d.ts.map +1 -1
  38. package/dist/mappers/response.js +13 -1
  39. package/dist/model-work-coordinator.d.ts +70 -0
  40. package/dist/model-work-coordinator.d.ts.map +1 -0
  41. package/dist/model-work-coordinator.js +120 -0
  42. package/dist/pending-writes.d.ts.map +1 -1
  43. package/dist/presets.d.ts +82 -0
  44. package/dist/presets.d.ts.map +1 -0
  45. package/dist/presets.js +98 -0
  46. package/dist/registry.d.ts +31 -1
  47. package/dist/registry.d.ts.map +1 -1
  48. package/dist/registry.js +33 -5
  49. package/dist/router.d.ts +4 -1
  50. package/dist/router.d.ts.map +1 -1
  51. package/dist/router.js +34 -4
  52. package/dist/server.d.ts +76 -0
  53. package/dist/server.d.ts.map +1 -1
  54. package/dist/server.js +48 -1
  55. package/dist/session-registry.d.ts +272 -18
  56. package/dist/session-registry.d.ts.map +1 -1
  57. package/dist/session-registry.js +509 -37
  58. package/dist/stop-sequence-buffer.d.ts +58 -0
  59. package/dist/stop-sequence-buffer.d.ts.map +1 -0
  60. package/dist/stop-sequence-buffer.js +148 -0
  61. package/dist/text-recovery.d.ts +35 -0
  62. package/dist/text-recovery.d.ts.map +1 -0
  63. package/dist/text-recovery.js +41 -0
  64. package/dist/timing.d.ts +80 -0
  65. package/dist/timing.d.ts.map +1 -0
  66. package/dist/timing.js +121 -0
  67. package/dist/tool-call-buffer.d.ts +5 -5
  68. package/dist/tool-call-buffer.d.ts.map +1 -1
  69. package/dist/tool-call-buffer.js +28 -8
  70. package/dist/types-anthropic.d.ts +161 -1
  71. package/dist/types-anthropic.d.ts.map +1 -1
  72. package/dist/types.d.ts +172 -2
  73. package/dist/types.d.ts.map +1 -1
  74. package/package.json +5 -5
@@ -1,4 +1,60 @@
1
1
  /** Anthropic Messages API request → internal `ChatMessage[]` + `ChatConfig`. */
2
+ import { anthropicToolUseIdToInternal } from './anthropic-response.js';
3
+ import { applyExtraBodyMtpOverrides } from './request.js';
4
+ /**
5
+ * Anthropic billing/attribution header prefix. Claude Code injects a leading
6
+ * system block of the shape `"x-anthropic-billing-header: cc_version=...; cch=<token>;"`
7
+ * where the `cch=` token rotates per request. Leaving this in the prompt
8
+ * defeats prefix caching at BOTH the warm-slot gate (`getOrCreateWarmAny`,
9
+ * which compares `requestedSystem` byte-equally) AND the native
10
+ * token-prefix verifier inside `chatSessionStart`. We mirror vLLM's strategy
11
+ * (`vllm/entrypoints/anthropic/serving.py`, commit 262b76a0, 2026-03-11):
12
+ * stateless, per-block, prefix-only — drop entirely BEFORE tokenization, so
13
+ * the model never sees the rotating token AND the byte-prefix is stable.
14
+ *
15
+ * Hardcoded (not configurable) and intentionally limited to this single
16
+ * prefix to mirror vLLM's exact behaviour. The string-system branch is left
17
+ * UNFILTERED to match upstream.
18
+ */
19
+ const ANTHROPIC_BILLING_HEADER_PREFIX = 'x-anthropic-billing-header';
20
+ /**
21
+ * Canonicalize the Anthropic `system` field into the same string the mapper
22
+ * bakes into the leading `system` ChatMessage. Used by both
23
+ * `mapAnthropicRequest` (so the model never sees the billing header) and the
24
+ * `/v1/messages` warm-slot gate cache-key derivation (so the gate matches
25
+ * across rotating billing tokens). The two views MUST stay in sync — a
26
+ * single source of truth prevents drift.
27
+ *
28
+ * Asymmetry vs. `mapAnthropicRequest`: the mapper THROWS on non-text blocks
29
+ * (it's a request-validation gate), but this helper silently skips them.
30
+ * Safe because `mapAnthropicRequest` runs first as a pre-flight gate, so by
31
+ * the time the cache-key is computed, the request shape has already been
32
+ * validated.
33
+ */
34
+ export function canonicalizeSystemForCacheKey(system) {
35
+ if (system == null)
36
+ return null;
37
+ if (typeof system === 'string')
38
+ return system;
39
+ const parts = [];
40
+ for (const b of system) {
41
+ if (b.type === 'text' && !b.text.startsWith(ANTHROPIC_BILLING_HEADER_PREFIX)) {
42
+ parts.push(b.text);
43
+ }
44
+ }
45
+ // An array whose every block is stripped (e.g. a request whose only system
46
+ // block is the rotating `x-anthropic-billing-header` line) is semantically
47
+ // equivalent to "no system" — collapse to `null` so it (a) does NOT push an
48
+ // empty `system` ChatMessage that the chat template wraps with two extra
49
+ // `<|im_start|>system\n<|im_end|>\n` tokens (perturbing the prefix vs. an
50
+ // absent-system request), and (b) compares byte-equal to `undefined` /
51
+ // missing on the warm-slot gate (`SessionRegistry.getOrCreateWarmAny`
52
+ // checks `entry.instructions !== requestedInstructions`, where `null !==
53
+ // ''` would otherwise miss the slot).
54
+ if (parts.length === 0)
55
+ return null;
56
+ return parts.join('');
57
+ }
2
58
  /**
3
59
  * Resolve the text content of a `tool_result` block. The internal `ChatMessage`
4
60
  * shape (NAPI-generated) has no `images` field on `role: 'tool'`, so nested
@@ -46,23 +102,48 @@ function mapTool(tool) {
46
102
  }
47
103
  export function mapAnthropicRequest(req) {
48
104
  const messages = [];
105
+ // The leading system message is assembled and `unshift`ed AFTER the message
106
+ // loop so that any `system`-role message folded out of `req.messages` (see
107
+ // the `role === 'system'` branch below) is concatenated with the top-level
108
+ // `system` field into a single leading system prompt. We compute the
109
+ // top-level contribution here but defer the push.
110
+ let topLevelSystem = null;
49
111
  if (req.system != null) {
50
112
  if (typeof req.system === 'string') {
51
- messages.push({ role: 'system', content: req.system });
113
+ topLevelSystem = req.system;
52
114
  }
53
115
  else {
54
- const systemParts = [];
116
+ // Validate first — throw early on unsupported block types so the
117
+ // request fails fast (this is the validation gate). Stripping of
118
+ // the rotating billing-header prefix happens inside
119
+ // `canonicalizeSystemForCacheKey`, the single source of truth
120
+ // shared with `endpoints/messages.ts`'s `requestedSystem` cache
121
+ // key. Routing both call sites through the same helper means
122
+ // any future change to the strip semantics (prefix list,
123
+ // normalization, etc.) lands in exactly one place — the mapped
124
+ // messages and the cache-key view cannot drift.
55
125
  for (const b of req.system) {
56
- if (b.type === 'text') {
57
- systemParts.push(b.text);
58
- }
59
- else {
126
+ if (b.type !== 'text') {
60
127
  throw new Error(`Unsupported system block type: "${b.type}"`);
61
128
  }
62
129
  }
63
- messages.push({ role: 'system', content: systemParts.join('') });
130
+ // Helper returns `null` when every block was stripped (e.g. a
131
+ // request whose only system block is the rotating
132
+ // `x-anthropic-billing-header` line). An all-stripped array is
133
+ // semantically equivalent to "no system", so it contributes
134
+ // nothing — emitting `{ role: 'system', content: '' }` would
135
+ // otherwise have the chat template wrap it with two extra
136
+ // `<|im_start|>system\n<|im_end|>\n` tokens, perturbing the
137
+ // prefix vs. an absent-system request and breaking prefix
138
+ // caching across the two semantically-equivalent shapes.
139
+ topLevelSystem = canonicalizeSystemForCacheKey(req.system);
64
140
  }
65
141
  }
142
+ // Text folded out of any `system`-role message(s) in `req.messages`, in
143
+ // encounter order. Anthropic has no system role in `messages`, but Claude
144
+ // Code hooks inject one; its content is positionless "additional context"
145
+ // so we fold it into the leading system prompt regardless of position.
146
+ const foldedSystemParts = [];
66
147
  for (const msg of req.messages) {
67
148
  const { role, content } = msg;
68
149
  if (role === 'user') {
@@ -82,6 +163,14 @@ export function mapAnthropicRequest(req) {
82
163
  const trailingImages = [];
83
164
  let seenNonToolResult = false;
84
165
  let seenToolResult = false;
166
+ // The flat `ChatMessage` shape cannot represent a text block that
167
+ // appears AFTER an image block in the same turn — the downstream
168
+ // Jinja serializer always places text before images. Reject that
169
+ // interleaving up front rather than silently reordering it and
170
+ // changing the caller's intent. This parallels the identical
171
+ // guard in `request.ts:resolveMessageContent` for the
172
+ // `/v1/responses` mapper.
173
+ let seenImage = false;
85
174
  for (const block of content) {
86
175
  if (block.type === 'tool_result') {
87
176
  if (seenNonToolResult) {
@@ -92,18 +181,32 @@ export function mapAnthropicRequest(req) {
92
181
  }
93
182
  seenToolResult = true;
94
183
  const resolved = resolveToolResultContent(block.content);
184
+ // Anthropic clients echo back the same `toolu_<uuid>` we
185
+ // emitted on the prior assistant turn. Translate it back to
186
+ // the internal `call_<uuid>` shape so the native session
187
+ // store's tool_call_id lookup (which sees the original
188
+ // `call_*` id) still matches. Ids that lack the `toolu_`
189
+ // prefix (legacy callers that already speak the internal
190
+ // shape) pass through unchanged.
95
191
  toolResults.push({
96
- toolCallId: block.tool_use_id,
192
+ toolCallId: anthropicToolUseIdToInternal(block.tool_use_id),
97
193
  content: resolved.text,
98
194
  isError: block.is_error === true,
99
195
  });
100
196
  }
101
197
  else if (block.type === 'text') {
198
+ if (seenImage) {
199
+ throw new Error('Unsupported: text block after an image block in the same user turn is not representable ' +
200
+ 'in the internal message model. The flat ChatMessage shape and the Jinja serializer both ' +
201
+ 'place all text before all images, so any mapping would silently reorder your content. ' +
202
+ 'Place all text blocks before any image blocks, or split across separate user turns.');
203
+ }
102
204
  seenNonToolResult = true;
103
205
  trailingText.push(block.text);
104
206
  }
105
207
  else if (block.type === 'image' && block.source.type === 'base64') {
106
208
  seenNonToolResult = true;
209
+ seenImage = true;
107
210
  trailingImages.push(Buffer.from(block.source.data, 'base64'));
108
211
  }
109
212
  else {
@@ -111,20 +214,32 @@ export function mapAnthropicRequest(req) {
111
214
  }
112
215
  }
113
216
  if (seenToolResult) {
114
- // `ChatMessage` (NAPI-generated) has no `isError` field, so
115
- // Anthropic's `tool_result.is_error=true` is encoded as a JSON
116
- // envelope `{ "is_error": true, "content": <original> }`. The
117
- // envelope preserves the raw payload verbatim (unlike a text
118
- // prefix, which would corrupt JSON payloads and collide with
119
- // strings that legitimately start with the prefix). Every other
120
- // wire shape is a successful tool result.
217
+ // The structured `isError` field on the internal `ChatMessage`
218
+ // is the authoritative signal of tool-call failure (mirroring
219
+ // the existing `toolCallId` pattern). Pass `tr.content`
220
+ // through verbatim no JSON envelope, no in-band marker — and
221
+ // surface the error condition via the dedicated structured
222
+ // field. The Rust-side wire-format renderers (Jinja serializer
223
+ // for the cold-start path, ChatML formatter for the fallback
224
+ // template) inject a short model-facing `[tool error]` cue
225
+ // into the prompt when `isError === true`, but the
226
+ // `ChatMessage.content` itself stays byte-for-byte equal to
227
+ // the original payload so a successful tool result whose
228
+ // content happens to start with the same marker text cannot
229
+ // be confused with an errored one on read-back. Neither
230
+ // mlx-lm nor mlx-vlm have a precedent for an in-band marker
231
+ // here; the structured-field approach matches how
232
+ // `toolCallId` is plumbed and survives round-tripping cleanly.
121
233
  for (const tr of toolResults) {
122
- const encoded = tr.isError ? JSON.stringify({ is_error: true, content: tr.content }) : tr.content;
123
- messages.push({
234
+ const msg = {
124
235
  role: 'tool',
125
- content: encoded,
236
+ content: tr.content,
126
237
  toolCallId: tr.toolCallId,
127
- });
238
+ };
239
+ if (tr.isError) {
240
+ msg.isError = true;
241
+ }
242
+ messages.push(msg);
128
243
  }
129
244
  // Trailing suffix after a tool_result prefix: accept either
130
245
  // (a) text-only (concatenated) or (b) exactly one image block.
@@ -186,8 +301,12 @@ export function mapAnthropicRequest(req) {
186
301
  }
187
302
  else if (block.type === 'tool_use') {
188
303
  seenToolUse = true;
304
+ // Anthropic clients echo back the `toolu_<uuid>` we emitted
305
+ // on the prior assistant turn. Translate to the internal
306
+ // `call_<uuid>` shape so the native session store and the
307
+ // Qwen chat template paths see a consistent id family.
189
308
  toolCalls.push({
190
- id: block.id,
309
+ id: anthropicToolUseIdToInternal(block.id),
191
310
  name: block.name,
192
311
  arguments: JSON.stringify(block.input),
193
312
  });
@@ -206,10 +325,68 @@ export function mapAnthropicRequest(req) {
206
325
  messages.push(assistantMsg);
207
326
  }
208
327
  }
328
+ else if (role === 'system') {
329
+ // `system` is not a role in the Anthropic Messages spec, but Claude
330
+ // Code's SessionStart hooks (e.g. superpowers) inject a
331
+ // `{ role: 'system' }` message carrying "additional context" into the
332
+ // `messages` array. Rather than rejecting the request (HTTP 400), fold
333
+ // its text into the leading system prompt (assembled after the loop).
334
+ //
335
+ // CONTRACT — position-agnostic hoist (deliberate): a `system`-role
336
+ // message is folded to the SINGLE leading system prompt regardless of
337
+ // where it appears in `messages`. This is intentional, not incidental:
338
+ // 1. The Anthropic wire format has no positional `system` role, so
339
+ // any `{ role: 'system' }` here is non-spec tooling injection with
340
+ // no defined positional semantics to preserve.
341
+ // 2. The only known producer (Claude Code SessionStart hooks) emits
342
+ // positionless "additional context" — conceptually system-level,
343
+ // not a turn-point instruction.
344
+ // 3. The internal `ChatMessage`/`primeHistory` pipeline represents
345
+ // only a SINGLE leading system message; a mid-history system turn
346
+ // is not representable, so hoisting is the sole non-rejecting
347
+ // option. Multiple system-role messages accumulate in encounter
348
+ // order (handled at assembly below).
349
+ if (typeof content === 'string') {
350
+ foldedSystemParts.push(content);
351
+ }
352
+ else {
353
+ let text = '';
354
+ for (const block of content) {
355
+ if (block.type !== 'text') {
356
+ throw new Error(`Unsupported content block type "${block.type}" in system-role message`);
357
+ }
358
+ text += block.text;
359
+ }
360
+ foldedSystemParts.push(text);
361
+ }
362
+ }
209
363
  else {
210
364
  throw new Error(`Unsupported message role: "${role}"`);
211
365
  }
212
366
  }
367
+ // Assemble the single leading system prompt from the top-level `system`
368
+ // field plus any folded `system`-role message text. Joined with `'\n\n'`
369
+ // between distinct contributions (a single string when there is only one),
370
+ // so a request with ONLY a top-level system — the overwhelmingly common
371
+ // case — stays byte-identical to the pre-folding behaviour.
372
+ //
373
+ // Empty folded contributions are dropped so an empty hook context message
374
+ // ({ role: 'system', content: '' }) neither corrupts a real top-level
375
+ // system prompt with a trailing `'\n\n'` separator nor synthesises a bare
376
+ // empty system message. The top-level `system` field itself is preserved
377
+ // verbatim (an explicit empty string still emits, matching prior behaviour).
378
+ const systemParts = [];
379
+ if (topLevelSystem !== null) {
380
+ systemParts.push(topLevelSystem);
381
+ }
382
+ for (const part of foldedSystemParts) {
383
+ if (part.length > 0) {
384
+ systemParts.push(part);
385
+ }
386
+ }
387
+ if (systemParts.length > 0) {
388
+ messages.unshift({ role: 'system', content: systemParts.join('\n\n') });
389
+ }
213
390
  const config = {
214
391
  reportPerformance: true,
215
392
  };
@@ -225,17 +402,38 @@ export function mapAnthropicRequest(req) {
225
402
  if (req.top_k != null) {
226
403
  config.topK = req.top_k;
227
404
  }
405
+ // `stop_sequences` has no `ChatConfig` field to map onto, so it rides out on
406
+ // the widened return instead. Normalize to drop absent/null entries, empty
407
+ // strings (which would match at every position and stop generation
408
+ // immediately), and whitespace-only entries (which would truncate normal
409
+ // output at the first space/newline; the real Anthropic API rejects these
410
+ // with a 400, so making them a no-op is the lowest-risk resolution). A
411
+ // downstream consumer honours the result.
412
+ const stopSequences = (req.stop_sequences ?? []).filter((s) => typeof s === 'string' && s.trim().length > 0);
228
413
  if (req.tools && req.tools.length > 0) {
229
414
  const toolChoice = req.tool_choice;
230
- if (toolChoice?.type === 'tool' && toolChoice.name) {
415
+ if (toolChoice?.type === 'tool') {
416
+ // `{type:'tool', name:'X'}` is a HARD constraint: the model MUST call X
417
+ // and only X. If the caller omitted the name, or named a tool that is
418
+ // not in `req.tools`, falling through to the all-tools path would
419
+ // silently violate that contract. Reject up front so the failure mode
420
+ // is loud and the client gets a clear 400.
421
+ if (!toolChoice.name) {
422
+ throw new Error('tool_choice.type is "tool" but no name was provided');
423
+ }
231
424
  const matched = req.tools.filter((t) => t.name === toolChoice.name);
232
- if (matched.length > 0) {
233
- config.tools = matched.map(mapTool);
425
+ if (matched.length === 0) {
426
+ throw new Error(`tool_choice references tool "${toolChoice.name}" which is not present in the request's tools list`);
234
427
  }
428
+ config.tools = matched.map(mapTool);
235
429
  }
236
430
  else {
431
+ // `tool_choice` is undefined, `{type:'auto'}`, or `{type:'any'}` — all
432
+ // three semantically mean "let the model pick from any tool", so we
433
+ // forward the full tools array.
237
434
  config.tools = req.tools.map(mapTool);
238
435
  }
239
436
  }
240
- return { messages, config };
437
+ applyExtraBodyMtpOverrides(config, req.extra_body);
438
+ return { messages, config, stopSequences };
241
439
  }
@@ -1,14 +1,39 @@
1
1
  /** ChatResult / ChatStreamEvent → Anthropic Messages API output. */
2
2
  import type { ChatResult } from '@mlx-node/core';
3
+ import { type PerformanceMetricsForUsage, type ServerTimingForUsage } from '../timing.js';
3
4
  import type { AnthropicContentBlockDeltaEvent, AnthropicContentBlockStartEvent, AnthropicContentBlockStopEvent, AnthropicDelta, AnthropicMessageDeltaEvent, AnthropicMessageStartEvent, AnthropicMessageStopEvent, AnthropicMessagesRequest, AnthropicMessagesResponse, AnthropicResponseContent } from '../types-anthropic.js';
4
- export declare function mapStopReason(finishReason: string, hasToolCalls: boolean): 'end_turn' | 'max_tokens' | 'tool_use';
5
- export declare function buildAnthropicContent(result: ChatResult): AnthropicResponseContent[];
6
- export declare function buildAnthropicResponse(result: ChatResult, req: AnthropicMessagesRequest, messageId: string): AnthropicMessagesResponse;
5
+ /**
6
+ * Translate a native tool-call id (minted as `call_<uuid>` by the Rust
7
+ * parser, which keeps the OpenAI Responses convention) to the Anthropic
8
+ * Messages wire convention (`toolu_<uuid>`). The uuid body is preserved
9
+ * verbatim so the inverse `anthropicToolUseIdToInternal` round-trips
10
+ * losslessly when clients echo the id back via `tool_result.tool_use_id`.
11
+ *
12
+ * Defensive: ids that do not have the expected `call_` prefix (e.g. a
13
+ * legacy caller, an in-process driver constructing its own id, or a
14
+ * future variant) pass through unchanged so this function never
15
+ * synthesizes a wrong id.
16
+ */
17
+ export declare function internalToolCallIdToAnthropic(id: string): string;
18
+ /**
19
+ * Inverse of `internalToolCallIdToAnthropic`: translate an incoming
20
+ * Anthropic `tool_use_id` (`toolu_<uuid>`) back to the internal `call_*`
21
+ * shape so historical assistant turns and the tool_call_id lookup in
22
+ * the native session store keep matching. Ids without the expected
23
+ * `toolu_` prefix pass through unchanged for the same defensive reason
24
+ * (some legacy callers send raw `call_*` directly).
25
+ */
26
+ export declare function anthropicToolUseIdToInternal(id: string): string;
27
+ export declare function mapStopReason(finishReason: string, hasToolCalls: boolean, matchedStopSequence?: string | null): 'end_turn' | 'max_tokens' | 'stop_sequence' | 'tool_use';
28
+ export declare function containsToolCallMarkup(rawText: string): boolean;
29
+ export declare function recoverSuppressedToolCallText(rawText: string): string;
30
+ export declare function buildAnthropicContent(result: ChatResult, allowToolUse?: boolean, stopMatched?: boolean): AnthropicResponseContent[];
31
+ export declare function buildAnthropicResponse(result: ChatResult, req: AnthropicMessagesRequest, messageId: string, performance?: PerformanceMetricsForUsage, allowToolUse?: boolean, serverTiming?: ServerTimingForUsage, matchedStopSequence?: string | null): AnthropicMessagesResponse;
7
32
  /** Embedded message has empty content and zero output_tokens at start. */
8
33
  export declare function buildMessageStartEvent(req: AnthropicMessagesRequest, messageId: string, inputTokens: number): AnthropicMessageStartEvent;
9
34
  export declare function buildContentBlockStart(index: number, block: AnthropicResponseContent): AnthropicContentBlockStartEvent;
10
35
  export declare function buildContentBlockDelta(index: number, delta: AnthropicDelta): AnthropicContentBlockDeltaEvent;
11
36
  export declare function buildContentBlockStop(index: number): AnthropicContentBlockStopEvent;
12
- export declare function buildMessageDelta(stopReason: string, outputTokens: number, inputTokens?: number): AnthropicMessageDeltaEvent;
37
+ export declare function buildMessageDelta(stopReason: string, outputTokens: number, inputTokens?: number, cachedTokens?: number, performance?: PerformanceMetricsForUsage, serverTiming?: ServerTimingForUsage, stopSequence?: string | null): AnthropicMessageDeltaEvent;
13
38
  export declare function buildMessageStop(): AnthropicMessageStopEvent;
14
39
  //# sourceMappingURL=anthropic-response.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"anthropic-response.d.ts","sourceRoot":"","sources":["../../src/mappers/anthropic-response.ts"],"names":[],"mappings":"AAAA,oEAAoE;AAEpE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,KAAK,EACV,+BAA+B,EAC/B,+BAA+B,EAC/B,8BAA8B,EAC9B,cAAc,EACd,0BAA0B,EAC1B,0BAA0B,EAC1B,yBAAyB,EACzB,wBAAwB,EACxB,yBAAyB,EACzB,wBAAwB,EACzB,MAAM,uBAAuB,CAAC;AAU/B,wBAAgB,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,GAAG,UAAU,GAAG,YAAY,GAAG,UAAU,CAQjH;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,UAAU,GAAG,wBAAwB,EAAE,CAwBpF;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,wBAAwB,EAC7B,SAAS,EAAE,MAAM,GAChB,yBAAyB,CAiB3B;AAID,0EAA0E;AAC1E,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,wBAAwB,EAC7B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAClB,0BAA0B,CAiB5B;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,wBAAwB,GAC9B,+BAA+B,CAMjC;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,+BAA+B,CAM5G;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,8BAA8B,CAKnF;AAED,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,WAAW,CAAC,EAAE,MAAM,GACnB,0BAA0B,CAY5B;AAED,wBAAgB,gBAAgB,IAAI,yBAAyB,CAI5D"}
1
+ {"version":3,"file":"anthropic-response.d.ts","sourceRoot":"","sources":["../../src/mappers/anthropic-response.ts"],"names":[],"mappings":"AAAA,oEAAoE;AAEpE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,OAAO,EAA8B,KAAK,0BAA0B,EAAE,KAAK,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACtH,OAAO,KAAK,EACV,+BAA+B,EAC/B,+BAA+B,EAC/B,8BAA8B,EAC9B,cAAc,EACd,0BAA0B,EAC1B,0BAA0B,EAC1B,yBAAyB,EACzB,wBAAwB,EACxB,yBAAyB,EACzB,wBAAwB,EACzB,MAAM,uBAAuB,CAAC;AAU/B;;;;;;;;;;;GAWG;AACH,wBAAgB,6BAA6B,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,wBAAgB,aAAa,CAC3B,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,OAAO,EACrB,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,GAClC,UAAU,GAAG,YAAY,GAAG,eAAe,GAAG,UAAU,CAW1D;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAO/D;AAED,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAUrE;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,UAAU,EAClB,YAAY,UAAO,EACnB,WAAW,UAAQ,GAClB,wBAAwB,EAAE,CA0C5B;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,wBAAwB,EAC7B,SAAS,EAAE,MAAM,EACjB,WAAW,CAAC,EAAE,0BAA0B,EACxC,YAAY,UAAO,EACnB,YAAY,CAAC,EAAE,oBAAoB,EACnC,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,GAClC,yBAAyB,CAyD3B;AAID,0EAA0E;AAC1E,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,wBAAwB,EAC7B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAClB,0BAA0B,CAiB5B;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,wBAAwB,GAC9B,+BAA+B,CAMjC;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,+BAA+B,CAM5G;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,8BAA8B,CAKnF;AAED,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,WAAW,CAAC,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,MAAM,EACrB,WAAW,CAAC,EAAE,0BAA0B,EACxC,YAAY,CAAC,EAAE,oBAAoB,EACnC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAC3B,0BAA0B,CAgC5B;AAED,wBAAgB,gBAAgB,IAAI,yBAAyB,CAI5D"}
@@ -1,4 +1,5 @@
1
1
  /** ChatResult / ChatStreamEvent → Anthropic Messages API output. */
2
+ import { mergeTimingUsageExtensions } from '../timing.js';
2
3
  import { genId } from './response.js';
3
4
  function parseArguments(args) {
4
5
  if (typeof args === 'string') {
@@ -6,7 +7,36 @@ function parseArguments(args) {
6
7
  }
7
8
  return args;
8
9
  }
9
- export function mapStopReason(finishReason, hasToolCalls) {
10
+ /**
11
+ * Translate a native tool-call id (minted as `call_<uuid>` by the Rust
12
+ * parser, which keeps the OpenAI Responses convention) to the Anthropic
13
+ * Messages wire convention (`toolu_<uuid>`). The uuid body is preserved
14
+ * verbatim so the inverse `anthropicToolUseIdToInternal` round-trips
15
+ * losslessly when clients echo the id back via `tool_result.tool_use_id`.
16
+ *
17
+ * Defensive: ids that do not have the expected `call_` prefix (e.g. a
18
+ * legacy caller, an in-process driver constructing its own id, or a
19
+ * future variant) pass through unchanged so this function never
20
+ * synthesizes a wrong id.
21
+ */
22
+ export function internalToolCallIdToAnthropic(id) {
23
+ return id.startsWith('call_') ? `toolu_${id.slice('call_'.length)}` : id;
24
+ }
25
+ /**
26
+ * Inverse of `internalToolCallIdToAnthropic`: translate an incoming
27
+ * Anthropic `tool_use_id` (`toolu_<uuid>`) back to the internal `call_*`
28
+ * shape so historical assistant turns and the tool_call_id lookup in
29
+ * the native session store keep matching. Ids without the expected
30
+ * `toolu_` prefix pass through unchanged for the same defensive reason
31
+ * (some legacy callers send raw `call_*` directly).
32
+ */
33
+ export function anthropicToolUseIdToInternal(id) {
34
+ return id.startsWith('toolu_') ? `call_${id.slice('toolu_'.length)}` : id;
35
+ }
36
+ export function mapStopReason(finishReason, hasToolCalls, matchedStopSequence) {
37
+ if (matchedStopSequence) {
38
+ return 'stop_sequence';
39
+ }
10
40
  if (finishReason === 'length') {
11
41
  return 'max_tokens';
12
42
  }
@@ -15,41 +45,112 @@ export function mapStopReason(finishReason, hasToolCalls) {
15
45
  }
16
46
  return 'end_turn';
17
47
  }
18
- export function buildAnthropicContent(result) {
48
+ export function containsToolCallMarkup(rawText) {
49
+ return (rawText.includes('<tool_call') ||
50
+ rawText.includes('</tool_call') ||
51
+ rawText.includes('<|tool_call') ||
52
+ rawText.includes('<tool_call|>'));
53
+ }
54
+ export function recoverSuppressedToolCallText(rawText) {
55
+ return rawText
56
+ .replace(/<\|channel>[\s\S]*?(?:<channel\|>|$)/g, '')
57
+ .replace(/<channel\|>/g, '')
58
+ .replace(/<\|tool_call>[\s\S]*?(?:<tool_call\|>|$)/g, '')
59
+ .replace(/<tool_call>[\s\S]*?(?:<\/tool_call>|$)/g, '')
60
+ .replace(/<\|tool_response>[\s\S]*?(?:<tool_response\|>|$)/g, '')
61
+ .replace(/<\|tool>[\s\S]*?(?:<tool\|>|$)/g, '')
62
+ .replace(/<\|turn>[^\n]*(?:\n|$)/g, '')
63
+ .replace(/<turn\|>/g, '');
64
+ }
65
+ export function buildAnthropicContent(result, allowToolUse = true, stopMatched = false) {
19
66
  const content = [];
20
67
  if (result.thinking) {
21
68
  content.push({ type: 'thinking', thinking: result.thinking });
22
69
  }
23
- const okToolCalls = result.toolCalls.filter((t) => t.status === 'ok');
70
+ const parsedToolCalls = result.toolCalls.filter((t) => t.status === 'ok');
71
+ // A matched stop sequence halts generation at its position, so any tool call
72
+ // whose tag would have followed the stop boundary is dropped and the
73
+ // truncated visible text (`result.text`, already cut at the stop) is emitted
74
+ // verbatim — the suppressed-markup recovery is skipped because it would
75
+ // re-introduce text that lived after the stop.
76
+ const okToolCalls = stopMatched || !allowToolUse ? [] : parsedToolCalls;
77
+ const text = !stopMatched &&
78
+ !allowToolUse &&
79
+ result.text.length === 0 &&
80
+ parsedToolCalls.length > 0 &&
81
+ containsToolCallMarkup(result.rawText)
82
+ ? recoverSuppressedToolCallText(result.rawText)
83
+ : result.text;
24
84
  // Emit a text block unless tool calls exist and there is no text.
25
- if (result.text || okToolCalls.length === 0) {
26
- content.push({ type: 'text', text: result.text });
85
+ if (text || okToolCalls.length === 0) {
86
+ content.push({ type: 'text', text });
27
87
  }
28
88
  for (const tc of okToolCalls) {
89
+ // Translate native `call_<uuid>` to Anthropic `toolu_<uuid>` at the
90
+ // wire boundary. The fallback `genId('toolu_')` covers the case where
91
+ // the native parser did not mint an id (an in-process driver or a
92
+ // legacy bridge — present-day Rust paths always populate it).
29
93
  content.push({
30
94
  type: 'tool_use',
31
- id: tc.id ?? genId('toolu_'),
95
+ id: tc.id != null ? internalToolCallIdToAnthropic(tc.id) : genId('toolu_'),
32
96
  name: tc.name,
33
97
  input: parseArguments(tc.arguments),
34
98
  });
35
99
  }
36
100
  return content;
37
101
  }
38
- export function buildAnthropicResponse(result, req, messageId) {
39
- const okToolCalls = result.toolCalls.filter((t) => t.status === 'ok');
102
+ export function buildAnthropicResponse(result, req, messageId, performance, allowToolUse = true, serverTiming, matchedStopSequence) {
103
+ // A matched stop sequence takes precedence over tool_use: suppress the tool
104
+ // calls so `stop_reason: 'stop_sequence'` is never emitted alongside a
105
+ // tool_use block whose tag followed the stop boundary.
106
+ const stopMatched = Boolean(matchedStopSequence);
107
+ const okToolCalls = allowToolUse && !stopMatched ? result.toolCalls.filter((t) => t.status === 'ok') : [];
40
108
  const hasToolCalls = okToolCalls.length > 0;
109
+ // Cache accounting (Anthropic Messages API spec):
110
+ // * On a cache HIT (`cachedTokens > 0`) the wire MUST emit
111
+ // `cache_read_input_tokens: cachedTokens` and reduce
112
+ // `input_tokens` to the unsuffixed remainder
113
+ // `promptTokens - cachedTokens` — Claude Code (and other
114
+ // Anthropic-compatible UIs) read this directly for cost /
115
+ // billing display, and a wire that left `input_tokens` at the
116
+ // full prompt count would silently double-bill the cached
117
+ // prefix.
118
+ // * On a cache MISS (`cachedTokens === 0`) the cache fields are
119
+ // OMITTED — they are optional in the spec and other
120
+ // Anthropic-compatible servers elide them on misses.
121
+ // * `cache_creation_input_tokens` stays unset: this server's KV
122
+ // reuse is implicit (no `cache_control` breakpoints), so a
123
+ // client that did not request explicit caching should never
124
+ // see a non-zero creation count.
125
+ const cachedTokens = result.cachedTokens;
126
+ const usage = cachedTokens > 0
127
+ ? {
128
+ input_tokens: result.promptTokens - cachedTokens,
129
+ output_tokens: result.numTokens,
130
+ cache_read_input_tokens: cachedTokens,
131
+ }
132
+ : {
133
+ input_tokens: result.promptTokens,
134
+ output_tokens: result.numTokens,
135
+ };
136
+ // Server-extension perf fields. Same gating pattern as
137
+ // `cache_read_input_tokens`: only land on the wire when the native
138
+ // dispatch produced a finite, positive value — `undefined` /
139
+ // `NaN` / `0` is elided so the launcher's verbose log can read
140
+ // absence as "not plumbed" instead of treating zero as a real
141
+ // measurement. Cache-context fields make the prefill rate explicit:
142
+ // on cached-prefix turns the denominator is the uncached suffix, not
143
+ // the full logical prompt.
144
+ mergeTimingUsageExtensions(usage, performance, result.promptTokens, result.numTokens, cachedTokens, serverTiming);
41
145
  return {
42
146
  id: messageId,
43
147
  type: 'message',
44
148
  role: 'assistant',
45
149
  model: req.model,
46
- content: buildAnthropicContent(result),
47
- stop_reason: mapStopReason(result.finishReason, hasToolCalls),
48
- stop_sequence: null,
49
- usage: {
50
- input_tokens: result.promptTokens,
51
- output_tokens: result.numTokens,
52
- },
150
+ content: buildAnthropicContent(result, allowToolUse, stopMatched),
151
+ stop_reason: mapStopReason(result.finishReason, hasToolCalls, matchedStopSequence),
152
+ stop_sequence: matchedStopSequence ?? null,
153
+ usage,
53
154
  };
54
155
  }
55
156
  // Streaming helpers
@@ -92,17 +193,38 @@ export function buildContentBlockStop(index) {
92
193
  index,
93
194
  };
94
195
  }
95
- export function buildMessageDelta(stopReason, outputTokens, inputTokens) {
196
+ export function buildMessageDelta(stopReason, outputTokens, inputTokens, cachedTokens, performance, serverTiming, stopSequence) {
197
+ // Streaming `message_delta` mirrors the non-streaming response's
198
+ // cache accounting: when `cachedTokens > 0` we emit
199
+ // `cache_read_input_tokens: cachedTokens` AND subtract that count
200
+ // from `input_tokens`. On a cache miss (or when `cachedTokens` is
201
+ // omitted by an in-process driver / mock) the cache fields stay
202
+ // off the wire. See the matching block on `buildAnthropicResponse`
203
+ // and the field-level docstrings on `AnthropicUsage`.
204
+ const usage = {
205
+ output_tokens: outputTokens,
206
+ };
207
+ if (cachedTokens != null && cachedTokens > 0) {
208
+ if (inputTokens != null) {
209
+ usage.input_tokens = inputTokens - cachedTokens;
210
+ }
211
+ usage.cache_read_input_tokens = cachedTokens;
212
+ }
213
+ else if (inputTokens != null) {
214
+ usage.input_tokens = inputTokens;
215
+ }
216
+ // Server-extension perf fields — same gating pattern as the
217
+ // cache-field block above. See `buildAnthropicResponse` for the
218
+ // matching non-streaming branch and the docstring on
219
+ // `AnthropicUsage` for the wire-format rationale.
220
+ mergeTimingUsageExtensions(usage, performance, inputTokens, outputTokens, cachedTokens, serverTiming);
96
221
  return {
97
222
  type: 'message_delta',
98
223
  delta: {
99
224
  stop_reason: stopReason,
100
- stop_sequence: null,
101
- },
102
- usage: {
103
- ...(inputTokens != null ? { input_tokens: inputTokens } : {}),
104
- output_tokens: outputTokens,
225
+ stop_sequence: stopSequence ?? null,
105
226
  },
227
+ usage,
106
228
  };
107
229
  }
108
230
  export function buildMessageStop() {