@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,78 @@
1
+ /**
2
+ * Buffers streaming text to detect and suppress model structural tags. Text
3
+ * that cannot be part of a partial tag is released immediately; once a
4
+ * full structural tag is seen, everything after it is suppressed until
5
+ * the stream ends.
6
+ */
7
+ export class ToolCallTagBuffer {
8
+ private static readonly TAGS = [
9
+ '<tool_call>',
10
+ '</tool_call>',
11
+ '<|tool_call>',
12
+ '<tool_call|>',
13
+ '<|tool_response>',
14
+ '<tool_response|>',
15
+ '<|tool>',
16
+ '<tool|>',
17
+ '<|channel>',
18
+ '<channel|>',
19
+ '<|turn>',
20
+ '<turn|>',
21
+ ] as const;
22
+ private pendingText = '';
23
+ private _suppressed = false;
24
+
25
+ get suppressed(): boolean {
26
+ return this._suppressed;
27
+ }
28
+
29
+ /**
30
+ * Feed text in. Returns `safeText` (emit as delta), `tagFound` (a full
31
+ * structural tag was just seen), and `cleanPrefix` (text before the tag
32
+ * when `tagFound` — may contain whitespace; use `.trim()` only for
33
+ * emptiness checks, never for emission).
34
+ */
35
+ push(text: string): { safeText: string; tagFound: boolean; cleanPrefix: string } {
36
+ if (this._suppressed) {
37
+ return { safeText: '', tagFound: false, cleanPrefix: '' };
38
+ }
39
+
40
+ this.pendingText += text;
41
+
42
+ let tagIdx = -1;
43
+ for (const tag of ToolCallTagBuffer.TAGS) {
44
+ const idx = this.pendingText.indexOf(tag);
45
+ if (idx >= 0 && (tagIdx < 0 || idx < tagIdx)) {
46
+ tagIdx = idx;
47
+ }
48
+ }
49
+ if (tagIdx >= 0) {
50
+ const cleanPrefix = this.pendingText.slice(0, tagIdx);
51
+ this._suppressed = true;
52
+ this.pendingText = '';
53
+ return { safeText: '', tagFound: true, cleanPrefix };
54
+ }
55
+
56
+ // Hold back any suffix that could be the start of the tag.
57
+ let safeLen = this.pendingText.length;
58
+ const maxTagLength = Math.max(...ToolCallTagBuffer.TAGS.map((tag) => tag.length));
59
+ for (let i = 1; i <= Math.min(this.pendingText.length, maxTagLength - 1); i++) {
60
+ const suffix = this.pendingText.slice(-i);
61
+ if (ToolCallTagBuffer.TAGS.some((tag) => tag.startsWith(suffix))) {
62
+ safeLen = this.pendingText.length - i;
63
+ break;
64
+ }
65
+ }
66
+
67
+ const safeText = this.pendingText.slice(0, safeLen);
68
+ this.pendingText = this.pendingText.slice(safeLen);
69
+ return { safeText, tagFound: false, cleanPrefix: '' };
70
+ }
71
+
72
+ /** Release any held-back text at stream end. */
73
+ flush(): string {
74
+ const text = this.pendingText;
75
+ this.pendingText = '';
76
+ return text;
77
+ }
78
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Transport visibility tracking shared between the Responses and Messages
3
+ * endpoints. Gates "safe to suppress" on whether the client actually observed
4
+ * a terminal artefact for this turn.
5
+ *
6
+ * The helpers reject — and leave visibility flags unflipped — on ANY of:
7
+ * - the write callback reporting err != null;
8
+ * - a `'error'` event on `res`;
9
+ * - a `'close'` event on `res` or its socket;
10
+ * - a pre-write check finding the response/socket already destroyed.
11
+ *
12
+ * Flipping the flags from `res.end()` / `writeSSEEvent`'s synchronous return
13
+ * is not sufficient: on a dead socket `_writeRaw` can return without ever
14
+ * firing the callback or `'error'`, which would pin the per-model mutex on a
15
+ * client that cannot see anything we write.
16
+ *
17
+ * Non-terminal SSE write calls remain synchronous, but streaming handlers
18
+ * await a close-safe drain promise whenever one reports backpressure. Only the
19
+ * terminal event uses the callback-backed visibility flush. Streaming handlers
20
+ * independently attach a `res.once('close', …)` listener to flip
21
+ * `clientAborted` so the decode loop breaks at the next iteration boundary.
22
+ */
23
+
24
+ import type { ServerResponse } from 'node:http';
25
+
26
+ import { writeSSEEvent } from './streaming.js';
27
+
28
+ /**
29
+ * Wire format committed by the handler. `null` = pre-headers (outer catch
30
+ * can still emit a clean 500 JSON). `'json'` = `writeHead(200, 'application/json')`
31
+ * already fired — the outer catch MUST NOT emit SSE frames. `'sse'` = `beginSSE()`
32
+ * fired — the outer catch may emit a best-effort streaming `error` event.
33
+ */
34
+ export type ResponseMode = 'json' | 'sse' | null;
35
+
36
+ export interface TransportVisibility {
37
+ responseMode: ResponseMode;
38
+ /** Set only after `res.end(body)`'s callback fires with err == null — proves kernel acceptance, not buffer queue. */
39
+ responseBodyWritten: boolean;
40
+ /** Set only after the terminal SSE event's write callback reports no error. */
41
+ terminalEmitted: boolean;
42
+ }
43
+
44
+ export function createVisibility(): TransportVisibility {
45
+ return {
46
+ responseMode: null,
47
+ responseBodyWritten: false,
48
+ terminalEmitted: false,
49
+ };
50
+ }
51
+
52
+ /** True if `res` or its socket is already destroyed — writes will never be seen and callbacks may never fire. */
53
+ function isSocketGone(res: ServerResponse): boolean {
54
+ if (res.destroyed) return true;
55
+ const sock = res.socket;
56
+ if (sock != null && sock.destroyed) return true;
57
+ return false;
58
+ }
59
+
60
+ /**
61
+ * Write an HTTP 200 JSON response and await kernel ack via `res.end(body, cb)`.
62
+ * `responseMode` is committed AFTER `writeHead` returns so a synchronous throw
63
+ * from `writeHead` leaves `responseMode === null` for the outer catch.
64
+ * `responseBodyWritten` is flipped only on the callback's success path.
65
+ */
66
+ export async function endJson(res: ServerResponse, body: string, visibility: TransportVisibility): Promise<void> {
67
+ res.writeHead(200, { 'Content-Type': 'application/json' });
68
+ visibility.responseMode = 'json';
69
+ await new Promise<void>((resolve, reject) => {
70
+ let settled = false;
71
+ const onError = (err: Error): void => {
72
+ settle(err instanceof Error ? err : new Error(String(err)));
73
+ };
74
+ const onClose = (): void => {
75
+ // `'close'` without a successful end callback = client never saw the body.
76
+ settle(new Error('response closed before write completion'));
77
+ };
78
+ const sock = res.socket;
79
+ const settle = (err: Error | null): void => {
80
+ if (settled) return;
81
+ settled = true;
82
+ res.removeListener('error', onError);
83
+ res.removeListener('close', onClose);
84
+ if (sock != null) sock.removeListener('close', onClose);
85
+ if (err != null) {
86
+ reject(err);
87
+ } else {
88
+ visibility.responseBodyWritten = true;
89
+ resolve();
90
+ }
91
+ };
92
+
93
+ // Pre-check: `'close'` may have already fired and the write callback
94
+ // may never arrive on a destroyed peer — reject synchronously instead.
95
+ if (isSocketGone(res)) {
96
+ settle(new Error('response socket already destroyed before write'));
97
+ return;
98
+ }
99
+
100
+ res.once('error', onError);
101
+ res.once('close', onClose);
102
+ if (sock != null) sock.once('close', onClose);
103
+
104
+ try {
105
+ res.end(body, (err?: Error | null) => {
106
+ settle(err ?? null);
107
+ });
108
+ } catch (err) {
109
+ settle(err instanceof Error ? err : new Error(String(err)));
110
+ }
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Emit the terminal SSE event for a streaming response and await kernel ack via
116
+ * `res.write(chunk, cb)`. `terminalEmitted` is flipped only on the success path.
117
+ * Used for `response.completed` / `response.failed`, `message_stop`, and the
118
+ * streaming `error` event. Non-terminal writes stay synchronous.
119
+ */
120
+ export async function flushTerminalSSE(
121
+ res: ServerResponse,
122
+ eventType: string,
123
+ data: object,
124
+ visibility: TransportVisibility,
125
+ ): Promise<void> {
126
+ const payload = { type: eventType, ...data };
127
+ const chunk = `event: ${eventType}\ndata: ${JSON.stringify(payload)}\n\n`;
128
+ await new Promise<void>((resolve, reject) => {
129
+ let settled = false;
130
+ const onError = (err: Error): void => {
131
+ settle(err instanceof Error ? err : new Error(String(err)));
132
+ };
133
+ const onClose = (): void => {
134
+ settle(new Error('response closed before terminal SSE write completion'));
135
+ };
136
+ const sock = res.socket;
137
+ const settle = (err: Error | null): void => {
138
+ if (settled) return;
139
+ settled = true;
140
+ res.removeListener('error', onError);
141
+ res.removeListener('close', onClose);
142
+ if (sock != null) sock.removeListener('close', onClose);
143
+ if (err != null) {
144
+ reject(err);
145
+ } else {
146
+ visibility.terminalEmitted = true;
147
+ resolve();
148
+ }
149
+ };
150
+
151
+ if (isSocketGone(res)) {
152
+ settle(new Error('response socket already destroyed before terminal SSE write'));
153
+ return;
154
+ }
155
+
156
+ res.once('error', onError);
157
+ res.once('close', onClose);
158
+ if (sock != null) sock.once('close', onClose);
159
+
160
+ try {
161
+ // Ignore backpressure return value — the callback + `'close'` listener
162
+ // + `isSocketGone` pre-check cover every settle path we need.
163
+ const ok = res.write(chunk, (err?: Error | null) => {
164
+ settle(err ?? null);
165
+ });
166
+ void ok;
167
+ } catch (err) {
168
+ settle(err instanceof Error ? err : new Error(String(err)));
169
+ }
170
+ });
171
+ }
172
+
173
+ /** Commit to SSE mode — call immediately after `beginSSE(res)` so the outer catch routes SSE-shaped failures correctly. */
174
+ export function markSSEMode(visibility: TransportVisibility): void {
175
+ visibility.responseMode = 'sse';
176
+ }
177
+
178
+ /** Best-effort synchronous SSE `error` event for the outer catch when the handler threw before flushing a terminal. */
179
+ export function writeFallbackErrorSSE(res: ServerResponse, eventType: string, data: object): void {
180
+ try {
181
+ writeSSEEvent(res, eventType, data);
182
+ } catch {
183
+ // Socket is gone — let the caller complete the lifecycle via `end` / destroy.
184
+ }
185
+ }
@@ -0,0 +1,409 @@
1
+ /** Anthropic Messages API types: request/response shapes and SSE streaming events for POST /v1/messages. */
2
+
3
+ export interface AnthropicTextContentBlock {
4
+ type: 'text';
5
+ text: string;
6
+ }
7
+
8
+ export interface AnthropicImageContentBlock {
9
+ type: 'image';
10
+ source: {
11
+ type: 'base64';
12
+ media_type: string;
13
+ data: string;
14
+ };
15
+ }
16
+
17
+ export interface AnthropicToolResultContentBlock {
18
+ type: 'tool_result';
19
+ tool_use_id: string;
20
+ /** May be a string, text-block array, or mix of text and image blocks. Image-mixed shapes are rejected by the mapper; see `resolveToolResultContent`. */
21
+ content?: string | (AnthropicTextContentBlock | AnthropicImageContentBlock)[];
22
+ is_error?: boolean;
23
+ }
24
+
25
+ export interface AnthropicToolUseContentBlock {
26
+ type: 'tool_use';
27
+ id: string;
28
+ name: string;
29
+ input: Record<string, unknown>;
30
+ }
31
+
32
+ export interface AnthropicThinkingContentBlock {
33
+ type: 'thinking';
34
+ thinking: string;
35
+ }
36
+
37
+ export type AnthropicContentBlock =
38
+ | AnthropicTextContentBlock
39
+ | AnthropicImageContentBlock
40
+ | AnthropicToolResultContentBlock
41
+ | AnthropicToolUseContentBlock
42
+ | AnthropicThinkingContentBlock;
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // System block
46
+ // ---------------------------------------------------------------------------
47
+
48
+ export interface SystemBlock {
49
+ type: 'text';
50
+ text: string;
51
+ cache_control?: { type: string };
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Messages
56
+ // ---------------------------------------------------------------------------
57
+
58
+ export interface AnthropicMessage {
59
+ // `system` is not part of the Anthropic Messages spec (system prompts go in
60
+ // the top-level `system` field), but Claude Code's SessionStart hooks inject
61
+ // a `{ role: 'system' }` message carrying "additional context" into the
62
+ // `messages` array. We tolerate it by folding its text into the system
63
+ // prompt (see `mapAnthropicRequest`) rather than rejecting the request.
64
+ role: 'user' | 'assistant' | 'system';
65
+ content: string | AnthropicContentBlock[];
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Tools
70
+ // ---------------------------------------------------------------------------
71
+
72
+ export interface AnthropicToolDefinition {
73
+ name: string;
74
+ description?: string;
75
+ input_schema: Record<string, unknown>;
76
+ }
77
+
78
+ export interface AnthropicToolChoice {
79
+ type: 'auto' | 'any' | 'tool';
80
+ name?: string;
81
+ disable_parallel_tool_use?: boolean;
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Request
86
+ // ---------------------------------------------------------------------------
87
+
88
+ export interface AnthropicMessagesRequest {
89
+ model: string;
90
+ messages: AnthropicMessage[];
91
+ max_tokens: number;
92
+ system?: string | SystemBlock[];
93
+ temperature?: number;
94
+ top_p?: number;
95
+ top_k?: number;
96
+ tools?: AnthropicToolDefinition[];
97
+ tool_choice?: AnthropicToolChoice;
98
+ stream?: boolean;
99
+ stop_sequences?: string[];
100
+ metadata?: { user_id?: string };
101
+ /**
102
+ * MLX-Node extension matching vLLM's Anthropic surface. Separates native
103
+ * content-addressed prefix-cache reuse between security domains. The value
104
+ * must be non-empty, at most 256 UTF-8 bytes, stable for the tenant, secret,
105
+ * and unpredictable.
106
+ */
107
+ cache_salt?: string;
108
+ output_config?: {
109
+ format?: {
110
+ type?: string;
111
+ schema?: unknown;
112
+ };
113
+ };
114
+ // NOTE: `prompt_cache_key` is intentionally NOT advertised on this
115
+ // endpoint. KV-cache reuse on `/v1/messages` is delivered via the
116
+ // server-side `getOrCreateWarmAny` warm-slot mechanism keyed on the
117
+ // mapped system/instructions string and a per-model sentinel id —
118
+ // see the block comment on `endpoints/messages.ts`. Paged-active
119
+ // models (Qwen3 / LFM2 / Gemma4, default-on) additionally benefit
120
+ // from native `BlockAllocator` content-addressed prefix reuse, which
121
+ // recovers shared SYS/user prefixes across requests without any
122
+ // client-supplied key. The `prompt_cache_key` field is therefore
123
+ // unnecessary on this surface and re-adding it without a per-key
124
+ // tier-2 path on the handler would be a no-op that silently misleads
125
+ // clients. The equivalent field on `/v1/responses` is still honoured
126
+ // for that endpoint's tier-2 lookup.
127
+ /**
128
+ * MLX-Node extension carrier for non-Anthropic fields, namespaced
129
+ * under `extra_body` to mirror the OpenAI-side `/v1/responses`
130
+ * surface. Unknown keys are ignored (additive, forward-compat).
131
+ *
132
+ * Currently exposes:
133
+ * * `generation_mode`: `"mtp"` forces W6 speculative-decode (sets
134
+ * `enableMtp = true`), `"ar"` forces plain autoregressive
135
+ * (`enableMtp = false`). Absent / null / unrecognized leaves
136
+ * `enableMtp` untouched so the downstream `ChatSession` auto-
137
+ * default (true when the model ships an MTP head) applies.
138
+ * * `mtp_depth`: positive integer override for the per-call draft
139
+ * depth. The server forwards any positive integer ≤ 64 (a sanity
140
+ * ceiling that only blocks garbage) and the native per-family
141
+ * `resolve_params` owns the real clamps: qwen3.5 native MTP
142
+ * [1, 5], gemma4 DSpark capped at the draft block size, gemma4
143
+ * assistant drafts [1, 8].
144
+ */
145
+ extra_body?: {
146
+ // Typed as `string | null` (not the literal union `'mtp' | 'ar'`)
147
+ // because the value arrives off-wire and may carry any client-
148
+ // supplied payload. The mapper validates by exact-string match;
149
+ // anything that doesn't match is silently ignored so the auto-
150
+ // default still applies.
151
+ generation_mode?: string | null;
152
+ mtp_depth?: number | null;
153
+ };
154
+ }
155
+
156
+ export type AnthropicCountTokensRequest = Omit<AnthropicMessagesRequest, 'max_tokens'> & {
157
+ max_tokens?: number;
158
+ };
159
+
160
+ export interface AnthropicCountTokensResponse {
161
+ input_tokens: number;
162
+ }
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Response content blocks
166
+ // ---------------------------------------------------------------------------
167
+
168
+ export interface AnthropicResponseTextBlock {
169
+ type: 'text';
170
+ text: string;
171
+ }
172
+
173
+ export interface AnthropicResponseThinkingBlock {
174
+ type: 'thinking';
175
+ thinking: string;
176
+ }
177
+
178
+ export interface AnthropicResponseToolUseBlock {
179
+ type: 'tool_use';
180
+ id: string;
181
+ name: string;
182
+ input: Record<string, unknown>;
183
+ }
184
+
185
+ export type AnthropicResponseContent =
186
+ | AnthropicResponseTextBlock
187
+ | AnthropicResponseThinkingBlock
188
+ | AnthropicResponseToolUseBlock;
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // Usage
192
+ // ---------------------------------------------------------------------------
193
+
194
+ /**
195
+ * Anthropic Messages API `usage` block.
196
+ *
197
+ * Per the Anthropic spec the cache accounting fields are OPTIONAL and
198
+ * carry distinct semantics that drive client-side cost / billing
199
+ * displays (Claude Code reads `cache_read_input_tokens` directly):
200
+ *
201
+ * * `input_tokens` — prompt tokens that were processed at full cost
202
+ * this turn. When a cached prefix is reused, this MUST be reduced
203
+ * to the unsuffixed remainder (`promptTokens - cachedTokens`) so
204
+ * the client doesn't double-count the cached prefix.
205
+ * * `cache_read_input_tokens` — prompt tokens served from a cached
206
+ * prefix on this turn. Emitted only when reuse genuinely happened
207
+ * (`cachedTokens > 0`); omitted on a cold turn so the wire matches
208
+ * other Anthropic-compatible servers that elide the field on cache
209
+ * misses.
210
+ * * `cache_creation_input_tokens` — prompt tokens written to a NEW
211
+ * cache entry. Currently always omitted: this server's KV reuse
212
+ * is implicit and has no `cache_control` breakpoints, so a client
213
+ * that did not request explicit caching should never see a
214
+ * non-zero creation count.
215
+ *
216
+ * The timing fields below are NON-Anthropic extension fields surfaced
217
+ * for the launcher's verbose log (`requests.ndjson`) so per-turn
218
+ * decode-rate / TTFT / prefill-rate telemetry rides the same response
219
+ * envelope. They describe native/server inference timing, not the
220
+ * HTTP logger's outer `elapsedMs` envelope. They are emitted only when
221
+ * the underlying native dispatch produced a finite, positive value —
222
+ * missing or non-finite metrics are elided rather than surfaced as
223
+ * zero / null. Anthropic-compatible clients (Claude Code, official
224
+ * Anthropic SDKs) ignore unknown fields, so the extension is wire-safe
225
+ * — it parallels how `cache_read_input_tokens` is treated above.
226
+ *
227
+ * `prefill_input_tokens` and `cached_prefix_tokens` provide the
228
+ * denominator/context for `prefill_tokens_per_second`: on cached-prefix
229
+ * turns the prefill rate is for the uncached suffix, not the full
230
+ * logical prompt.
231
+ */
232
+ export interface AnthropicUsage {
233
+ input_tokens: number;
234
+ output_tokens: number;
235
+ cache_read_input_tokens?: number;
236
+ cache_creation_input_tokens?: number;
237
+ /** Server-extension: time-to-first-token in milliseconds. */
238
+ time_to_first_token_ms?: number;
239
+ /** Server-extension: prompt-token throughput during prefill. */
240
+ prefill_tokens_per_second?: number;
241
+ /** Server-extension: generated-token throughput during decode. */
242
+ decode_tokens_per_second?: number;
243
+ /** Server-extension: native/server inference elapsed, excluding HTTP transport and logging overhead. */
244
+ server_inference_elapsed_ms?: number;
245
+ /** Server-extension alias for disambiguating native TTFT from request/HTTP elapsed time. */
246
+ server_time_to_first_token_ms?: number;
247
+ /** Server-extension: handler-start to first native token, including model resolve/load and queue wait. */
248
+ server_total_time_to_first_token_ms?: number;
249
+ /** Server-extension alias for native prefill throughput. */
250
+ server_prefill_tokens_per_second?: number;
251
+ /** Server-extension alias for native decode throughput. */
252
+ server_decode_tokens_per_second?: number;
253
+ /** Server-extension: prompt tokens actually prefetched this turn after cached-prefix reuse. */
254
+ prefill_input_tokens?: number;
255
+ /** Server-extension: prompt tokens skipped because a cached prefix was reused. */
256
+ cached_prefix_tokens?: number;
257
+ /** Server-extension: time spent resolving/loading/aliasing the requested model before registry lookup. */
258
+ server_model_resolve_ms?: number;
259
+ /** Server-extension: wall-clock time blocked on the process-wide model-load writer lock. */
260
+ server_load_wait_ms?: number;
261
+ /** Server-extension: true when this request acquired the model-load lock without contention. */
262
+ server_load_owner?: boolean;
263
+ /** Server-extension: time spent waiting behind the per-model execution mutex. */
264
+ server_queue_ms?: number;
265
+ /** Server-extension: handler time before native inference begins, including resolve and queue wait. */
266
+ server_pre_inference_ms?: number;
267
+ /** Server-extension: effective process-level paged-prefill chunk size. */
268
+ server_paged_prefill_chunk_size?: number;
269
+ /** Server-extension: effective process-level paged-prefill eval/clear cadence. */
270
+ server_paged_prefill_eval_interval?: number;
271
+ /** Server-extension: effective process-level paged-decode cache-clear cadence. */
272
+ server_paged_decode_cache_clear_interval?: number;
273
+ }
274
+
275
+ // ---------------------------------------------------------------------------
276
+ // Response
277
+ // ---------------------------------------------------------------------------
278
+
279
+ export interface AnthropicMessagesResponse {
280
+ id: string;
281
+ type: 'message';
282
+ role: 'assistant';
283
+ model: string;
284
+ content: AnthropicResponseContent[];
285
+ stop_reason: 'end_turn' | 'max_tokens' | 'stop_sequence' | 'tool_use' | null;
286
+ stop_sequence: string | null;
287
+ usage: AnthropicUsage;
288
+ }
289
+
290
+ // ---------------------------------------------------------------------------
291
+ // Streaming delta types
292
+ // ---------------------------------------------------------------------------
293
+
294
+ export interface AnthropicTextDelta {
295
+ type: 'text_delta';
296
+ text: string;
297
+ }
298
+
299
+ export interface AnthropicThinkingDelta {
300
+ type: 'thinking_delta';
301
+ thinking: string;
302
+ }
303
+
304
+ export interface AnthropicInputJsonDelta {
305
+ type: 'input_json_delta';
306
+ partial_json: string;
307
+ }
308
+
309
+ export type AnthropicDelta = AnthropicTextDelta | AnthropicThinkingDelta | AnthropicInputJsonDelta;
310
+
311
+ // ---------------------------------------------------------------------------
312
+ // Streaming events
313
+ // ---------------------------------------------------------------------------
314
+
315
+ export interface AnthropicMessageStartEvent {
316
+ type: 'message_start';
317
+ message: AnthropicMessagesResponse;
318
+ }
319
+
320
+ export interface AnthropicContentBlockStartEvent {
321
+ type: 'content_block_start';
322
+ index: number;
323
+ content_block: AnthropicResponseContent;
324
+ }
325
+
326
+ export interface AnthropicContentBlockDeltaEvent {
327
+ type: 'content_block_delta';
328
+ index: number;
329
+ delta: AnthropicDelta;
330
+ }
331
+
332
+ export interface AnthropicContentBlockStopEvent {
333
+ type: 'content_block_stop';
334
+ index: number;
335
+ }
336
+
337
+ export interface AnthropicMessageDeltaEvent {
338
+ type: 'message_delta';
339
+ delta: {
340
+ stop_reason: string | null;
341
+ stop_sequence: string | null;
342
+ };
343
+ /**
344
+ * Streaming `message_delta` carries the SAME cache-accounting
345
+ * semantics as the non-streaming response `usage` block (see
346
+ * `AnthropicUsage` above) — `input_tokens` MUST be net of any reused
347
+ * prefix, and `cache_read_input_tokens` is emitted only on a true
348
+ * reuse turn.
349
+ *
350
+ * Timing/accounting fields are server extensions (not in
351
+ * Anthropic's spec) surfaced for the launcher's verbose log; see
352
+ * the docstring on `AnthropicUsage`. Clients that do not recognize
353
+ * them ignore them.
354
+ */
355
+ usage: {
356
+ input_tokens?: number;
357
+ output_tokens: number;
358
+ cache_read_input_tokens?: number;
359
+ cache_creation_input_tokens?: number;
360
+ /** Server-extension: time-to-first-token in milliseconds. */
361
+ time_to_first_token_ms?: number;
362
+ /** Server-extension: prompt-token throughput during prefill. */
363
+ prefill_tokens_per_second?: number;
364
+ /** Server-extension: generated-token throughput during decode. */
365
+ decode_tokens_per_second?: number;
366
+ /** Server-extension: native/server inference elapsed, excluding HTTP transport and logging overhead. */
367
+ server_inference_elapsed_ms?: number;
368
+ /** Server-extension alias for disambiguating native TTFT from request/HTTP elapsed time. */
369
+ server_time_to_first_token_ms?: number;
370
+ /** Server-extension: handler-start to first native token, including model resolve/load and queue wait. */
371
+ server_total_time_to_first_token_ms?: number;
372
+ /** Server-extension alias for native prefill throughput. */
373
+ server_prefill_tokens_per_second?: number;
374
+ /** Server-extension alias for native decode throughput. */
375
+ server_decode_tokens_per_second?: number;
376
+ /** Server-extension: prompt tokens actually prefetched this turn after cached-prefix reuse. */
377
+ prefill_input_tokens?: number;
378
+ /** Server-extension: prompt tokens skipped because a cached prefix was reused. */
379
+ cached_prefix_tokens?: number;
380
+ /** Server-extension: time spent resolving/loading/aliasing the requested model before registry lookup. */
381
+ server_model_resolve_ms?: number;
382
+ /** Server-extension: wall-clock time blocked on the process-wide model-load writer lock. */
383
+ server_load_wait_ms?: number;
384
+ /** Server-extension: true when this request acquired the model-load lock without contention. */
385
+ server_load_owner?: boolean;
386
+ /** Server-extension: time spent waiting behind the per-model execution mutex. */
387
+ server_queue_ms?: number;
388
+ /** Server-extension: handler time before native inference begins, including resolve and queue wait. */
389
+ server_pre_inference_ms?: number;
390
+ /** Server-extension: effective process-level paged-prefill chunk size. */
391
+ server_paged_prefill_chunk_size?: number;
392
+ /** Server-extension: effective process-level paged-prefill eval/clear cadence. */
393
+ server_paged_prefill_eval_interval?: number;
394
+ /** Server-extension: effective process-level paged-decode cache-clear cadence. */
395
+ server_paged_decode_cache_clear_interval?: number;
396
+ };
397
+ }
398
+
399
+ export interface AnthropicMessageStopEvent {
400
+ type: 'message_stop';
401
+ }
402
+
403
+ export type AnthropicStreamEvent =
404
+ | AnthropicMessageStartEvent
405
+ | AnthropicContentBlockStartEvent
406
+ | AnthropicContentBlockDeltaEvent
407
+ | AnthropicContentBlockStopEvent
408
+ | AnthropicMessageDeltaEvent
409
+ | AnthropicMessageStopEvent;