@mlx-node/server 0.0.7 → 0.0.9
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/dist/auth.d.ts +56 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +106 -0
- package/dist/chat-session-warm-reuse.d.ts +51 -0
- package/dist/chat-session-warm-reuse.d.ts.map +1 -0
- package/dist/chat-session-warm-reuse.js +72 -0
- package/dist/endpoints/messages-count-tokens.d.ts +8 -0
- package/dist/endpoints/messages-count-tokens.d.ts.map +1 -0
- package/dist/endpoints/messages-count-tokens.js +121 -0
- package/dist/endpoints/messages.d.ts +57 -5
- package/dist/endpoints/messages.d.ts.map +1 -1
- package/dist/endpoints/messages.js +1043 -147
- package/dist/endpoints/models.d.ts +2 -1
- package/dist/endpoints/models.d.ts.map +1 -1
- package/dist/endpoints/models.js +2 -2
- package/dist/endpoints/responses.d.ts +22 -7
- package/dist/endpoints/responses.d.ts.map +1 -1
- package/dist/endpoints/responses.js +608 -85
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -0
- package/dist/handler.d.ts +69 -1
- package/dist/handler.d.ts.map +1 -1
- package/dist/handler.js +70 -16
- package/dist/health.d.ts +146 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +107 -0
- package/dist/host/discover.d.ts +19 -0
- package/dist/host/discover.d.ts.map +1 -0
- package/dist/host/discover.js +50 -0
- package/dist/host/env-policy.d.ts +62 -0
- package/dist/host/env-policy.d.ts.map +1 -0
- package/dist/host/env-policy.js +69 -0
- package/dist/host/index.d.ts +202 -0
- package/dist/host/index.d.ts.map +1 -0
- package/dist/host/index.js +325 -0
- package/dist/host/logger.d.ts +36 -0
- package/dist/host/logger.d.ts.map +1 -0
- package/dist/host/logger.js +376 -0
- package/dist/host/net.d.ts +65 -0
- package/dist/host/net.d.ts.map +1 -0
- package/dist/host/net.js +97 -0
- package/dist/host/paths.d.ts +28 -0
- package/dist/host/paths.d.ts.map +1 -0
- package/dist/host/paths.js +71 -0
- package/dist/host/swap.d.ts +27 -0
- package/dist/host/swap.d.ts.map +1 -0
- package/dist/host/swap.js +178 -0
- package/dist/host/temp-root.d.ts +57 -0
- package/dist/host/temp-root.d.ts.map +1 -0
- package/dist/host/temp-root.js +99 -0
- package/dist/idle-sweeper.d.ts +245 -0
- package/dist/idle-sweeper.d.ts.map +1 -0
- package/dist/idle-sweeper.js +408 -0
- package/dist/index.d.ts +19 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -1
- package/dist/load-model.d.ts +69 -0
- package/dist/load-model.d.ts.map +1 -0
- package/dist/load-model.js +63 -0
- package/dist/mappers/anthropic-request.d.ts +24 -2
- package/dist/mappers/anthropic-request.d.ts.map +1 -1
- package/dist/mappers/anthropic-request.js +222 -24
- package/dist/mappers/anthropic-response.d.ts +29 -4
- package/dist/mappers/anthropic-response.d.ts.map +1 -1
- package/dist/mappers/anthropic-response.js +143 -21
- package/dist/mappers/request.d.ts +48 -0
- package/dist/mappers/request.d.ts.map +1 -1
- package/dist/mappers/request.js +211 -35
- package/dist/mappers/response.d.ts.map +1 -1
- package/dist/mappers/response.js +13 -1
- package/dist/model-work-coordinator.d.ts +95 -0
- package/dist/model-work-coordinator.d.ts.map +1 -0
- package/dist/model-work-coordinator.js +201 -0
- package/dist/pending-writes.d.ts.map +1 -1
- package/dist/presets.d.ts +82 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +98 -0
- package/dist/registry.d.ts +31 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +33 -5
- package/dist/router.d.ts +37 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +79 -7
- package/dist/server.d.ts +193 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +173 -10
- package/dist/session-registry.d.ts +279 -18
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/session-registry.js +518 -37
- package/dist/stop-sequence-buffer.d.ts +58 -0
- package/dist/stop-sequence-buffer.d.ts.map +1 -0
- package/dist/stop-sequence-buffer.js +148 -0
- package/dist/streaming.d.ts +14 -0
- package/dist/streaming.d.ts.map +1 -1
- package/dist/streaming.js +45 -0
- package/dist/text-recovery.d.ts +35 -0
- package/dist/text-recovery.d.ts.map +1 -0
- package/dist/text-recovery.js +41 -0
- package/dist/timing.d.ts +80 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +121 -0
- package/dist/tool-call-buffer.d.ts +5 -5
- package/dist/tool-call-buffer.d.ts.map +1 -1
- package/dist/tool-call-buffer.js +28 -8
- package/dist/types-anthropic.d.ts +161 -1
- package/dist/types-anthropic.d.ts.map +1 -1
- package/dist/types.d.ts +172 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +15 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anthropic-request.d.ts","sourceRoot":"","sources":["../../src/mappers/anthropic-request.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAEhF,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAkB,MAAM,gBAAgB,CAAC;AAE9E,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"anthropic-request.d.ts","sourceRoot":"","sources":["../../src/mappers/anthropic-request.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAEhF,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAkB,MAAM,gBAAgB,CAAC;AAE9E,OAAO,KAAK,EAEV,2BAA2B,EAE3B,wBAAwB,EAIzB,MAAM,uBAAuB,CAAC;AAI/B,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,MAAM,EAAE,UAAU,CAAC;IACnB;;;;;OAKG;IACH,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAmBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,wBAAwB,CAAC,QAAQ,CAAC,GAAG,MAAM,GAAG,IAAI,CAoBvG;AAiDD,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,wBAAwB,GAAG,2BAA2B,GAC1D,sBAAsB,CA8VxB"}
|
|
@@ -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
|
-
|
|
113
|
+
topLevelSystem = req.system;
|
|
52
114
|
}
|
|
53
115
|
else {
|
|
54
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
// `
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
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
|
|
123
|
-
messages.push({
|
|
234
|
+
const msg = {
|
|
124
235
|
role: 'tool',
|
|
125
|
-
content:
|
|
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'
|
|
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
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
26
|
-
content.push({ type: 'text', 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
|
|
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
|
-
|
|
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() {
|