@mlx-node/server 0.0.12 → 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.
- package/dist/host/discover.d.ts +3 -6
- package/dist/host/discover.d.ts.map +1 -1
- package/dist/host/discover.js +9 -42
- package/dist/host/index.d.ts +2 -2
- package/dist/host/index.d.ts.map +1 -1
- package/dist/host/index.js +8 -1
- package/package.json +9 -4
- package/src/auth.ts +111 -0
- package/src/chat-session-warm-reuse.ts +96 -0
- package/src/endpoints/messages-count-tokens.ts +164 -0
- package/src/endpoints/messages.ts +1802 -0
- package/src/endpoints/models.ts +20 -0
- package/src/endpoints/responses.ts +3928 -0
- package/src/errors.ts +120 -0
- package/src/handler.ts +195 -0
- package/src/health.ts +213 -0
- package/src/host/discover.ts +25 -0
- package/src/host/env-policy.ts +81 -0
- package/src/host/index.ts +496 -0
- package/src/host/logger.ts +419 -0
- package/src/host/net.ts +100 -0
- package/src/host/paths.ts +77 -0
- package/src/host/swap.ts +200 -0
- package/src/host/temp-root.ts +110 -0
- package/src/idle-sweeper.ts +555 -0
- package/src/index.ts +114 -0
- package/src/load-model.ts +92 -0
- package/src/mappers/anthropic-request.ts +485 -0
- package/src/mappers/anthropic-response.ts +306 -0
- package/src/mappers/request.ts +456 -0
- package/src/mappers/response.ts +163 -0
- package/src/model-work-coordinator.ts +416 -0
- package/src/pending-writes.ts +481 -0
- package/src/registry.ts +691 -0
- package/src/router.ts +220 -0
- package/src/server.ts +579 -0
- package/src/session-registry.ts +1371 -0
- package/src/stop-sequence-buffer.ts +161 -0
- package/src/streaming.ts +205 -0
- package/src/text-recovery.ts +41 -0
- package/src/timing.ts +236 -0
- package/src/tool-call-buffer.ts +78 -0
- package/src/transport-visibility.ts +185 -0
- package/src/types-anthropic.ts +409 -0
- 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;
|