@mlx-node/server 0.0.0 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/chat-session-warm-reuse.d.ts +51 -0
  2. package/dist/chat-session-warm-reuse.d.ts.map +1 -0
  3. package/dist/chat-session-warm-reuse.js +68 -0
  4. package/dist/endpoints/messages-count-tokens.d.ts +8 -0
  5. package/dist/endpoints/messages-count-tokens.d.ts.map +1 -0
  6. package/dist/endpoints/messages-count-tokens.js +121 -0
  7. package/dist/endpoints/messages.d.ts +57 -5
  8. package/dist/endpoints/messages.d.ts.map +1 -1
  9. package/dist/endpoints/messages.js +1043 -147
  10. package/dist/endpoints/models.d.ts +2 -1
  11. package/dist/endpoints/models.d.ts.map +1 -1
  12. package/dist/endpoints/models.js +2 -2
  13. package/dist/endpoints/responses.d.ts +20 -7
  14. package/dist/endpoints/responses.d.ts.map +1 -1
  15. package/dist/endpoints/responses.js +572 -82
  16. package/dist/errors.d.ts +1 -0
  17. package/dist/errors.d.ts.map +1 -1
  18. package/dist/errors.js +3 -0
  19. package/dist/handler.d.ts +42 -0
  20. package/dist/handler.d.ts.map +1 -1
  21. package/dist/handler.js +6 -1
  22. package/dist/idle-sweeper.d.ts +245 -0
  23. package/dist/idle-sweeper.d.ts.map +1 -0
  24. package/dist/idle-sweeper.js +408 -0
  25. package/dist/index.d.ts +8 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +10 -0
  28. package/dist/mappers/anthropic-request.d.ts +24 -2
  29. package/dist/mappers/anthropic-request.d.ts.map +1 -1
  30. package/dist/mappers/anthropic-request.js +222 -24
  31. package/dist/mappers/anthropic-response.d.ts +29 -4
  32. package/dist/mappers/anthropic-response.d.ts.map +1 -1
  33. package/dist/mappers/anthropic-response.js +143 -21
  34. package/dist/mappers/request.d.ts +48 -0
  35. package/dist/mappers/request.d.ts.map +1 -1
  36. package/dist/mappers/request.js +211 -35
  37. package/dist/mappers/response.d.ts.map +1 -1
  38. package/dist/mappers/response.js +13 -1
  39. package/dist/model-work-coordinator.d.ts +70 -0
  40. package/dist/model-work-coordinator.d.ts.map +1 -0
  41. package/dist/model-work-coordinator.js +120 -0
  42. package/dist/pending-writes.d.ts.map +1 -1
  43. package/dist/presets.d.ts +82 -0
  44. package/dist/presets.d.ts.map +1 -0
  45. package/dist/presets.js +98 -0
  46. package/dist/registry.d.ts +31 -1
  47. package/dist/registry.d.ts.map +1 -1
  48. package/dist/registry.js +33 -5
  49. package/dist/router.d.ts +4 -1
  50. package/dist/router.d.ts.map +1 -1
  51. package/dist/router.js +34 -4
  52. package/dist/server.d.ts +76 -0
  53. package/dist/server.d.ts.map +1 -1
  54. package/dist/server.js +48 -1
  55. package/dist/session-registry.d.ts +272 -18
  56. package/dist/session-registry.d.ts.map +1 -1
  57. package/dist/session-registry.js +509 -37
  58. package/dist/stop-sequence-buffer.d.ts +58 -0
  59. package/dist/stop-sequence-buffer.d.ts.map +1 -0
  60. package/dist/stop-sequence-buffer.js +148 -0
  61. package/dist/text-recovery.d.ts +35 -0
  62. package/dist/text-recovery.d.ts.map +1 -0
  63. package/dist/text-recovery.js +41 -0
  64. package/dist/timing.d.ts +80 -0
  65. package/dist/timing.d.ts.map +1 -0
  66. package/dist/timing.js +121 -0
  67. package/dist/tool-call-buffer.d.ts +5 -5
  68. package/dist/tool-call-buffer.d.ts.map +1 -1
  69. package/dist/tool-call-buffer.js +28 -8
  70. package/dist/types-anthropic.d.ts +161 -1
  71. package/dist/types-anthropic.d.ts.map +1 -1
  72. package/dist/types.d.ts +172 -2
  73. package/dist/types.d.ts.map +1 -1
  74. package/package.json +5 -5
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Buffers streaming text to detect configured stop sequences. Text that
3
+ * cannot be part of a partial stop sequence is released immediately; a
4
+ * trailing suffix that could be the start of a stop sequence is held back
5
+ * until a later push resolves it or the stream is flushed. Once a full stop
6
+ * sequence is seen, everything after it is suppressed.
7
+ */
8
+ export declare class StopSequenceBuffer {
9
+ private readonly stopSequences;
10
+ private readonly maxLength;
11
+ private pending_;
12
+ private _matched;
13
+ constructor(stopSequences: string[]);
14
+ /**
15
+ * Earliest index wins; on a tie at the same index the longest wins. Returns
16
+ * `{ idx, seq }` for the winning stop, or `{ idx: -1, seq: null }` when none
17
+ * is present in `pending`.
18
+ */
19
+ private findMatch;
20
+ /** The stop sequence that has matched so far, or `null` if none has. */
21
+ get matched(): string | null;
22
+ /**
23
+ * The text currently held back (received but neither emitted nor matched).
24
+ * The streaming done-path reads this so it can scan the terminal/recovered
25
+ * text on the SAME buffer with the held partial still in place, and so it
26
+ * can reconstruct the full received-but-unemitted prefix for overlap math.
27
+ */
28
+ get pending(): string;
29
+ /**
30
+ * The earliest start index `j` in `[0, limit]` such that `pending.slice(j)`
31
+ * is a non-empty STRICT prefix of some configured stop — i.e. a partial that
32
+ * a later push could still grow into that stop. Returns -1 when no pending
33
+ * suffix at or before `limit` is viable. Scanning from the front yields the
34
+ * earliest start index, which is the one whose completed stop would win the
35
+ * earliest-index tiebreak. A suffix longer than `maxLength - 1` can never be
36
+ * a strict prefix of any stop, so the search starts no earlier than that.
37
+ */
38
+ private earliestViablePrefixIndex;
39
+ /**
40
+ * Feed text in. Returns `safeText` (emit as delta) and `matched` (the stop
41
+ * sequence that has been matched, or `null`). After a match every push
42
+ * returns empty `safeText` and keeps reporting the matched sequence.
43
+ */
44
+ push(text: string): {
45
+ safeText: string;
46
+ matched: string | null;
47
+ };
48
+ /**
49
+ * Release any held-back text at stream end. If a stop sequence already
50
+ * matched, nothing more is emitted; otherwise the residue could not
51
+ * complete any sequence and is released.
52
+ */
53
+ flush(): {
54
+ safeText: string;
55
+ matched: string | null;
56
+ };
57
+ }
58
+ //# sourceMappingURL=stop-sequence-buffer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stop-sequence-buffer.d.ts","sourceRoot":"","sources":["../src/stop-sequence-buffer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAW;IACzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAM;IACtB,OAAO,CAAC,QAAQ,CAAuB;IAEvC,YAAY,aAAa,EAAE,MAAM,EAAE,EAOlC;IAED;;;;OAIG;IACH,OAAO,CAAC,SAAS;IAgBjB,wEAAwE;IACxE,IAAI,OAAO,IAAI,MAAM,GAAG,IAAI,CAE3B;IAED;;;;;OAKG;IACH,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED;;;;;;;;OAQG;IACH,OAAO,CAAC,yBAAyB;IAejC;;;;OAIG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CA8C/D;IAED;;;;OAIG;IACH,KAAK,IAAI;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAmBpD;CACF"}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Buffers streaming text to detect configured stop sequences. Text that
3
+ * cannot be part of a partial stop sequence is released immediately; a
4
+ * trailing suffix that could be the start of a stop sequence is held back
5
+ * until a later push resolves it or the stream is flushed. Once a full stop
6
+ * sequence is seen, everything after it is suppressed.
7
+ */
8
+ export class StopSequenceBuffer {
9
+ stopSequences;
10
+ maxLength;
11
+ pending_ = '';
12
+ _matched = null;
13
+ constructor(stopSequences) {
14
+ // Drop empty AND whitespace-only entries: a whitespace-only stop would
15
+ // truncate normal output at the first space/newline, and the real
16
+ // Anthropic API rejects such stops outright. Mirrors the same trim filter
17
+ // in the request mapper so a whitespace-only configuration is a no-op.
18
+ this.stopSequences = stopSequences.filter((s) => s.trim().length > 0);
19
+ this.maxLength = this.stopSequences.reduce((max, s) => Math.max(max, s.length), 0);
20
+ }
21
+ /**
22
+ * Earliest index wins; on a tie at the same index the longest wins. Returns
23
+ * `{ idx, seq }` for the winning stop, or `{ idx: -1, seq: null }` when none
24
+ * is present in `pending`.
25
+ */
26
+ findMatch() {
27
+ let matchIdx = -1;
28
+ let matchSeq = null;
29
+ for (const seq of this.stopSequences) {
30
+ const idx = this.pending_.indexOf(seq);
31
+ if (idx < 0) {
32
+ continue;
33
+ }
34
+ if (matchIdx < 0 || idx < matchIdx || (idx === matchIdx && seq.length > (matchSeq?.length ?? 0))) {
35
+ matchIdx = idx;
36
+ matchSeq = seq;
37
+ }
38
+ }
39
+ return { idx: matchIdx, seq: matchSeq };
40
+ }
41
+ /** The stop sequence that has matched so far, or `null` if none has. */
42
+ get matched() {
43
+ return this._matched;
44
+ }
45
+ /**
46
+ * The text currently held back (received but neither emitted nor matched).
47
+ * The streaming done-path reads this so it can scan the terminal/recovered
48
+ * text on the SAME buffer with the held partial still in place, and so it
49
+ * can reconstruct the full received-but-unemitted prefix for overlap math.
50
+ */
51
+ get pending() {
52
+ return this.pending_;
53
+ }
54
+ /**
55
+ * The earliest start index `j` in `[0, limit]` such that `pending.slice(j)`
56
+ * is a non-empty STRICT prefix of some configured stop — i.e. a partial that
57
+ * a later push could still grow into that stop. Returns -1 when no pending
58
+ * suffix at or before `limit` is viable. Scanning from the front yields the
59
+ * earliest start index, which is the one whose completed stop would win the
60
+ * earliest-index tiebreak. A suffix longer than `maxLength - 1` can never be
61
+ * a strict prefix of any stop, so the search starts no earlier than that.
62
+ */
63
+ earliestViablePrefixIndex(limit) {
64
+ if (this.pending_.length === 0) {
65
+ return -1;
66
+ }
67
+ const lowerBound = Math.max(0, this.pending_.length - (this.maxLength - 1));
68
+ const upper = Math.min(limit, this.pending_.length - 1);
69
+ for (let j = lowerBound; j <= upper; j++) {
70
+ const suffix = this.pending_.slice(j);
71
+ if (this.stopSequences.some((seq) => suffix.length < seq.length && seq.startsWith(suffix))) {
72
+ return j;
73
+ }
74
+ }
75
+ return -1;
76
+ }
77
+ /**
78
+ * Feed text in. Returns `safeText` (emit as delta) and `matched` (the stop
79
+ * sequence that has been matched, or `null`). After a match every push
80
+ * returns empty `safeText` and keeps reporting the matched sequence.
81
+ */
82
+ push(text) {
83
+ if (this._matched !== null) {
84
+ return { safeText: '', matched: this._matched };
85
+ }
86
+ // Transparent pass-through when there is nothing to detect.
87
+ if (this.stopSequences.length === 0) {
88
+ return { safeText: text, matched: null };
89
+ }
90
+ this.pending_ += text;
91
+ const { idx: matchIdx, seq: matchSeq } = this.findMatch();
92
+ // Earliest start index of a still-growable stop prefix. When a full match
93
+ // exists we only look at or before it (`limit = matchIdx`): a viable prefix
94
+ // beginning AFTER the match would complete at a later index and lose the
95
+ // earliest-index tiebreak, and the bytes from the match onward are
96
+ // suppressed anyway. A viable prefix at or before the match could still
97
+ // complete into a stop that WINS (earlier index, or longer at the same
98
+ // index), so the match must be held. With no full match we consider the
99
+ // whole pending text.
100
+ const limit = matchIdx >= 0 ? matchIdx : this.pending_.length - 1;
101
+ const holdIdx = this.earliestViablePrefixIndex(limit);
102
+ if (matchIdx >= 0 && matchSeq !== null) {
103
+ if (holdIdx >= 0) {
104
+ // A longer/earlier stop could still complete from `holdIdx`; emit only
105
+ // the bytes before it and keep the rest pending for a later push or
106
+ // `flush()` to resolve.
107
+ const safeText = this.pending_.slice(0, holdIdx);
108
+ this.pending_ = this.pending_.slice(holdIdx);
109
+ return { safeText, matched: null };
110
+ }
111
+ const safeText = this.pending_.slice(0, matchIdx);
112
+ this._matched = matchSeq;
113
+ this.pending_ = '';
114
+ return { safeText, matched: matchSeq };
115
+ }
116
+ // No full match: release everything before the earliest viable prefix and
117
+ // hold that suffix back, since a later push could complete it.
118
+ const safeLen = holdIdx >= 0 ? holdIdx : this.pending_.length;
119
+ const safeText = this.pending_.slice(0, safeLen);
120
+ this.pending_ = this.pending_.slice(safeLen);
121
+ return { safeText, matched: null };
122
+ }
123
+ /**
124
+ * Release any held-back text at stream end. If a stop sequence already
125
+ * matched, nothing more is emitted; otherwise the residue could not
126
+ * complete any sequence and is released.
127
+ */
128
+ flush() {
129
+ if (this._matched !== null) {
130
+ return { safeText: '', matched: this._matched };
131
+ }
132
+ // The stream has ended, so any match `push()` held back for a possible
133
+ // longer same-index stop can no longer be extended — resolve it now.
134
+ // Re-scan `pending` for the earliest match (longest on tie) and commit it
135
+ // if present; otherwise the residue could not complete any sequence and is
136
+ // released verbatim.
137
+ const { idx: matchIdx, seq: matchSeq } = this.findMatch();
138
+ if (matchIdx >= 0 && matchSeq !== null) {
139
+ const safeText = this.pending_.slice(0, matchIdx);
140
+ this._matched = matchSeq;
141
+ this.pending_ = '';
142
+ return { safeText, matched: matchSeq };
143
+ }
144
+ const safeText = this.pending_;
145
+ this.pending_ = '';
146
+ return { safeText, matched: null };
147
+ }
148
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Streaming text-recovery helpers shared between the `/v1/messages` and
3
+ * `/v1/responses` endpoints.
4
+ *
5
+ * Both endpoints have a tool-call streaming recovery branch that has to
6
+ * compute the unsent suffix of `finalText` given that some prefix of the
7
+ * model's output may already have been streamed to the wire, but native-side
8
+ * string normalization makes `finalText` diverge from the streamed-prefix
9
+ * verbatim. Concrete divergences seen in practice:
10
+ *
11
+ * * The native side trims leading whitespace after `</think>` via
12
+ * `split_at_think_end`, so the streamed text can end in `"\n\n"` while
13
+ * `finalText` starts at `"<tool_call>"` (no overlap — emit `finalText`
14
+ * whole).
15
+ * * The native side `.trim()`s tool-tag-bracketed content boundaries, so
16
+ * the streamed text can have a trailing space that `finalText` lacks
17
+ * (also no overlap — emit `finalText` whole).
18
+ *
19
+ * Internal-only — not exported from `packages/server/src/index.ts`.
20
+ */
21
+ /**
22
+ * Find the largest k such that `streamed.endsWith(final.slice(0, k))`.
23
+ *
24
+ * Returns 0 when there is no overlap (caller emits `final` whole).
25
+ * Returns `final.length` when `final` is fully contained as a suffix of
26
+ * `streamed` (caller emits nothing).
27
+ *
28
+ * Used by the `/v1/messages` and `/v1/responses` streaming tool-call
29
+ * recovery branches to decide how much of `finalText` is already on the
30
+ * wire when native-side normalization (e.g. `.trim()`, post-`</think>`
31
+ * whitespace stripping) makes the streamed prefix diverge from the
32
+ * `finalText` prefix verbatim.
33
+ */
34
+ export declare function longestSuffixPrefixOverlap(streamed: string, final: string): number;
35
+ //# sourceMappingURL=text-recovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-recovery.d.ts","sourceRoot":"","sources":["../src/text-recovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAMlF"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Streaming text-recovery helpers shared between the `/v1/messages` and
3
+ * `/v1/responses` endpoints.
4
+ *
5
+ * Both endpoints have a tool-call streaming recovery branch that has to
6
+ * compute the unsent suffix of `finalText` given that some prefix of the
7
+ * model's output may already have been streamed to the wire, but native-side
8
+ * string normalization makes `finalText` diverge from the streamed-prefix
9
+ * verbatim. Concrete divergences seen in practice:
10
+ *
11
+ * * The native side trims leading whitespace after `</think>` via
12
+ * `split_at_think_end`, so the streamed text can end in `"\n\n"` while
13
+ * `finalText` starts at `"<tool_call>"` (no overlap — emit `finalText`
14
+ * whole).
15
+ * * The native side `.trim()`s tool-tag-bracketed content boundaries, so
16
+ * the streamed text can have a trailing space that `finalText` lacks
17
+ * (also no overlap — emit `finalText` whole).
18
+ *
19
+ * Internal-only — not exported from `packages/server/src/index.ts`.
20
+ */
21
+ /**
22
+ * Find the largest k such that `streamed.endsWith(final.slice(0, k))`.
23
+ *
24
+ * Returns 0 when there is no overlap (caller emits `final` whole).
25
+ * Returns `final.length` when `final` is fully contained as a suffix of
26
+ * `streamed` (caller emits nothing).
27
+ *
28
+ * Used by the `/v1/messages` and `/v1/responses` streaming tool-call
29
+ * recovery branches to decide how much of `finalText` is already on the
30
+ * wire when native-side normalization (e.g. `.trim()`, post-`</think>`
31
+ * whitespace stripping) makes the streamed prefix diverge from the
32
+ * `finalText` prefix verbatim.
33
+ */
34
+ export function longestSuffixPrefixOverlap(streamed, final) {
35
+ const max = Math.min(streamed.length, final.length);
36
+ for (let k = max; k > 0; k--) {
37
+ if (streamed.endsWith(final.slice(0, k)))
38
+ return k;
39
+ }
40
+ return 0;
41
+ }
@@ -0,0 +1,80 @@
1
+ /** Wire-safe server timing extensions derived from native performance metrics. */
2
+ export interface PerformanceMetricsForUsage {
3
+ ttftMs?: number;
4
+ prefillTokensPerSecond?: number;
5
+ decodeTokensPerSecond?: number;
6
+ }
7
+ interface TimingUsageExtensions {
8
+ /** Server-extension: native time-to-first-token in milliseconds. */
9
+ time_to_first_token_ms?: number;
10
+ /** Server-extension: prompt-token throughput for the tokens actually prefetched this turn. */
11
+ prefill_tokens_per_second?: number;
12
+ /** Server-extension: generated-token throughput during decode. */
13
+ decode_tokens_per_second?: number;
14
+ /** Server-extension: native/server inference elapsed, excluding HTTP transport and logging overhead. */
15
+ server_inference_elapsed_ms?: number;
16
+ /** Server-extension alias for disambiguating native TTFT from request/HTTP elapsed time. */
17
+ server_time_to_first_token_ms?: number;
18
+ /** Server-extension: handler-start to first native token, including model resolve/load and queue wait. */
19
+ server_total_time_to_first_token_ms?: number;
20
+ /** Server-extension alias for native prefill throughput. */
21
+ server_prefill_tokens_per_second?: number;
22
+ /** Server-extension alias for native decode throughput. */
23
+ server_decode_tokens_per_second?: number;
24
+ /** Server-extension: prompt tokens actually prefetched this turn after cached-prefix reuse. */
25
+ prefill_input_tokens?: number;
26
+ /** Server-extension: prompt tokens skipped because a cached prefix was reused. */
27
+ cached_prefix_tokens?: number;
28
+ /**
29
+ * Server-extension: time spent resolving/loading/aliasing the requested model
30
+ * before registry lookup. Includes both the synchronous lookup AND any time
31
+ * spent driving the load. Excludes time spent blocked behind a peer
32
+ * request's in-flight load — that wait is reported separately via
33
+ * `server_load_wait_ms` so a fast follower request is not mis-attributed
34
+ * a long resolve when it merely inherited a cold-load wait.
35
+ */
36
+ server_model_resolve_ms?: number;
37
+ /**
38
+ * Server-extension: wall-clock time this request spent blocked on the
39
+ * process-wide model-load writer lock. Set when the load was already
40
+ * in flight when this request arrived (a peer drove the load and we
41
+ * waited for it to finish) AND when this request itself drove the
42
+ * load. Inspect `server_load_owner` to disambiguate. When the writer
43
+ * lock was free and the resolve was a no-op (model already loaded),
44
+ * this field is elided.
45
+ */
46
+ server_load_wait_ms?: number;
47
+ /**
48
+ * Server-extension: `true` when this request acquired the model-load
49
+ * writer lock with no contention (i.e. either the model was already
50
+ * loaded and the call was a no-op, or this request itself drove the
51
+ * load). `false` when the request was parked behind a peer's in-flight
52
+ * load. Elided when no load coordinator is wired (single-process
53
+ * tests, embedded callers).
54
+ */
55
+ server_load_owner?: boolean;
56
+ /** Server-extension: time spent waiting behind the per-model execution mutex. */
57
+ server_queue_ms?: number;
58
+ /** Server-extension: handler time before native inference begins, including resolve and queue wait. */
59
+ server_pre_inference_ms?: number;
60
+ /** Server-extension: effective process-level paged-prefill chunk size. */
61
+ server_paged_prefill_chunk_size?: number;
62
+ /** Server-extension: effective process-level paged-prefill eval/clear cadence. */
63
+ server_paged_prefill_eval_interval?: number;
64
+ /** Server-extension: effective process-level paged-decode cache-clear cadence. */
65
+ server_paged_decode_cache_clear_interval?: number;
66
+ }
67
+ export interface ServerTimingForUsage {
68
+ server_model_resolve_ms?: number;
69
+ server_load_wait_ms?: number;
70
+ server_load_owner?: boolean;
71
+ server_queue_ms?: number;
72
+ server_pre_inference_ms?: number;
73
+ server_paged_prefill_chunk_size?: number;
74
+ server_paged_prefill_eval_interval?: number;
75
+ server_paged_decode_cache_clear_interval?: number;
76
+ }
77
+ export declare function resolveServerTuningForUsage(env?: Record<string, string | undefined>): Pick<ServerTimingForUsage, 'server_paged_prefill_chunk_size' | 'server_paged_prefill_eval_interval' | 'server_paged_decode_cache_clear_interval'>;
78
+ export declare function mergeTimingUsageExtensions<T extends TimingUsageExtensions>(usage: T, performance: PerformanceMetricsForUsage | undefined, promptTokens: number | undefined, outputTokens: number | undefined, cachedTokens: number | undefined, serverTiming?: ServerTimingForUsage): void;
79
+ export {};
80
+ //# sourceMappingURL=timing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timing.d.ts","sourceRoot":"","sources":["../src/timing.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAElF,MAAM,WAAW,0BAA0B;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,UAAU,qBAAqB;IAC7B,oEAAoE;IACpE,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,8FAA8F;IAC9F,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,kEAAkE;IAClE,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,wGAAwG;IACxG,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,4FAA4F;IAC5F,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,0GAA0G;IAC1G,mCAAmC,CAAC,EAAE,MAAM,CAAC;IAC7C,4DAA4D;IAC5D,gCAAgC,CAAC,EAAE,MAAM,CAAC;IAC1C,2DAA2D;IAC3D,+BAA+B,CAAC,EAAE,MAAM,CAAC;IACzC,+FAA+F;IAC/F,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,kFAAkF;IAClF,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC;;;;;;;;OAQG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,iFAAiF;IACjF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uGAAuG;IACvG,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,0EAA0E;IAC1E,+BAA+B,CAAC,EAAE,MAAM,CAAC;IACzC,kFAAkF;IAClF,kCAAkC,CAAC,EAAE,MAAM,CAAC;IAC5C,kFAAkF;IAClF,wCAAwC,CAAC,EAAE,MAAM,CAAC;CACnD;AAcD,MAAM,WAAW,oBAAoB;IACnC,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,+BAA+B,CAAC,EAAE,MAAM,CAAC;IACzC,kCAAkC,CAAC,EAAE,MAAM,CAAC;IAC5C,wCAAwC,CAAC,EAAE,MAAM,CAAC;CACnD;AAsBD,wBAAgB,2BAA2B,CACzC,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GACpD,IAAI,CACL,oBAAoB,EACpB,iCAAiC,GAAG,oCAAoC,GAAG,0CAA0C,CACtH,CAMA;AAuGD,wBAAgB,0BAA0B,CAAC,CAAC,SAAS,qBAAqB,EACxE,KAAK,EAAE,CAAC,EACR,WAAW,EAAE,0BAA0B,GAAG,SAAS,EACnD,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,YAAY,CAAC,EAAE,oBAAoB,GAClC,IAAI,CAEN"}
package/dist/timing.js ADDED
@@ -0,0 +1,121 @@
1
+ /** Wire-safe server timing extensions derived from native performance metrics. */
2
+ function finitePositive(value) {
3
+ return value != null && Number.isFinite(value) && value > 0 ? value : undefined;
4
+ }
5
+ function finiteNonNegativeInteger(value) {
6
+ return value != null && Number.isFinite(value) && value >= 0 ? Math.max(0, Math.floor(value)) : undefined;
7
+ }
8
+ function finiteNonNegative(value) {
9
+ return value != null && Number.isFinite(value) && value >= 0 ? value : undefined;
10
+ }
11
+ const I32_MAX = 0x7fff_ffff;
12
+ function parseI32(value) {
13
+ if (value == null)
14
+ return undefined;
15
+ const trimmed = value.trim();
16
+ if (!/^[+-]?\d+$/.test(trimmed))
17
+ return undefined;
18
+ const parsed = Number.parseInt(trimmed, 10);
19
+ return Number.isSafeInteger(parsed) && parsed >= -0x8000_0000 && parsed <= I32_MAX ? parsed : undefined;
20
+ }
21
+ function parseNonNegativeI32(value, fallback) {
22
+ const parsed = parseI32(value);
23
+ return parsed != null && parsed >= 0 ? parsed : fallback;
24
+ }
25
+ function parsePositiveI32(value, fallback) {
26
+ const parsed = parseI32(value);
27
+ return parsed != null && parsed > 0 ? parsed : fallback;
28
+ }
29
+ export function resolveServerTuningForUsage(env = process.env) {
30
+ return {
31
+ server_paged_prefill_chunk_size: parseNonNegativeI32(env.MLX_PAGED_PREFILL_CHUNK_SIZE, 0),
32
+ server_paged_prefill_eval_interval: parsePositiveI32(env.MLX_PAGED_PREFILL_EVAL_INTERVAL, 8),
33
+ server_paged_decode_cache_clear_interval: parsePositiveI32(env.MLX_PAGED_DECODE_CACHE_CLEAR_INTERVAL, 1024),
34
+ };
35
+ }
36
+ function computeServerInferenceElapsedMs(ttftMs, decodeTokensPerSecond, outputTokens) {
37
+ if (ttftMs == null)
38
+ return undefined;
39
+ const generatedTokens = finiteNonNegativeInteger(outputTokens);
40
+ if (generatedTokens == null) {
41
+ return ttftMs;
42
+ }
43
+ if (generatedTokens <= 1) {
44
+ return ttftMs;
45
+ }
46
+ if (decodeTokensPerSecond == null) {
47
+ return undefined;
48
+ }
49
+ return ttftMs + ((generatedTokens - 1) / decodeTokensPerSecond) * 1000;
50
+ }
51
+ function buildTimingUsageExtensions(performance, promptTokens, outputTokens, cachedTokens, serverTiming) {
52
+ const ttftMs = finitePositive(performance?.ttftMs);
53
+ const prefillTokensPerSecond = finitePositive(performance?.prefillTokensPerSecond);
54
+ const decodeTokensPerSecond = finitePositive(performance?.decodeTokensPerSecond);
55
+ const serverInferenceElapsedMs = computeServerInferenceElapsedMs(ttftMs, decodeTokensPerSecond, outputTokens);
56
+ const preInferenceMs = finiteNonNegative(serverTiming?.server_pre_inference_ms);
57
+ const extensions = {};
58
+ if (ttftMs != null) {
59
+ extensions.time_to_first_token_ms = ttftMs;
60
+ extensions.server_time_to_first_token_ms = ttftMs;
61
+ if (preInferenceMs != null) {
62
+ extensions.server_total_time_to_first_token_ms = preInferenceMs + ttftMs;
63
+ }
64
+ }
65
+ if (prefillTokensPerSecond != null) {
66
+ extensions.prefill_tokens_per_second = prefillTokensPerSecond;
67
+ extensions.server_prefill_tokens_per_second = prefillTokensPerSecond;
68
+ }
69
+ if (decodeTokensPerSecond != null) {
70
+ extensions.decode_tokens_per_second = decodeTokensPerSecond;
71
+ extensions.server_decode_tokens_per_second = decodeTokensPerSecond;
72
+ }
73
+ if (serverInferenceElapsedMs != null && Number.isFinite(serverInferenceElapsedMs) && serverInferenceElapsedMs > 0) {
74
+ extensions.server_inference_elapsed_ms = serverInferenceElapsedMs;
75
+ }
76
+ if (performance != null) {
77
+ const prompt = finiteNonNegativeInteger(promptTokens);
78
+ const cached = finiteNonNegativeInteger(cachedTokens);
79
+ if (prompt != null) {
80
+ const cachedPrefix = cached == null ? 0 : Math.min(cached, prompt);
81
+ extensions.prefill_input_tokens = prompt - cachedPrefix;
82
+ if (cachedPrefix > 0) {
83
+ extensions.cached_prefix_tokens = cachedPrefix;
84
+ }
85
+ }
86
+ }
87
+ const modelResolveMs = finiteNonNegative(serverTiming?.server_model_resolve_ms);
88
+ if (modelResolveMs != null) {
89
+ extensions.server_model_resolve_ms = modelResolveMs;
90
+ }
91
+ const loadWaitMs = finiteNonNegative(serverTiming?.server_load_wait_ms);
92
+ if (loadWaitMs != null) {
93
+ extensions.server_load_wait_ms = loadWaitMs;
94
+ }
95
+ if (typeof serverTiming?.server_load_owner === 'boolean') {
96
+ extensions.server_load_owner = serverTiming.server_load_owner;
97
+ }
98
+ const queueMs = finiteNonNegative(serverTiming?.server_queue_ms);
99
+ if (queueMs != null) {
100
+ extensions.server_queue_ms = queueMs;
101
+ }
102
+ if (preInferenceMs != null) {
103
+ extensions.server_pre_inference_ms = preInferenceMs;
104
+ }
105
+ const pagedPrefillChunkSize = finiteNonNegativeInteger(serverTiming?.server_paged_prefill_chunk_size);
106
+ if (pagedPrefillChunkSize != null) {
107
+ extensions.server_paged_prefill_chunk_size = pagedPrefillChunkSize;
108
+ }
109
+ const pagedPrefillEvalInterval = finiteNonNegativeInteger(serverTiming?.server_paged_prefill_eval_interval);
110
+ if (pagedPrefillEvalInterval != null) {
111
+ extensions.server_paged_prefill_eval_interval = pagedPrefillEvalInterval;
112
+ }
113
+ const pagedDecodeCacheClearInterval = finiteNonNegativeInteger(serverTiming?.server_paged_decode_cache_clear_interval);
114
+ if (pagedDecodeCacheClearInterval != null) {
115
+ extensions.server_paged_decode_cache_clear_interval = pagedDecodeCacheClearInterval;
116
+ }
117
+ return extensions;
118
+ }
119
+ export function mergeTimingUsageExtensions(usage, performance, promptTokens, outputTokens, cachedTokens, serverTiming) {
120
+ Object.assign(usage, buildTimingUsageExtensions(performance, promptTokens, outputTokens, cachedTokens, serverTiming));
121
+ }
@@ -1,17 +1,17 @@
1
1
  /**
2
- * Buffers streaming text to detect and suppress `<tool_call>` tags. Text
2
+ * Buffers streaming text to detect and suppress model structural tags. Text
3
3
  * that cannot be part of a partial tag is released immediately; once a
4
- * full tag is seen, everything after it is suppressed until the stream
5
- * ends.
4
+ * full structural tag is seen, everything after it is suppressed until
5
+ * the stream ends.
6
6
  */
7
7
  export declare class ToolCallTagBuffer {
8
- private static readonly TAG;
8
+ private static readonly TAGS;
9
9
  private pendingText;
10
10
  private _suppressed;
11
11
  get suppressed(): boolean;
12
12
  /**
13
13
  * Feed text in. Returns `safeText` (emit as delta), `tagFound` (a full
14
- * `<tool_call>` was just seen), and `cleanPrefix` (text before the tag
14
+ * structural tag was just seen), and `cleanPrefix` (text before the tag
15
15
  * when `tagFound` — may contain whitespace; use `.trim()` only for
16
16
  * emptiness checks, never for emission).
17
17
  */
@@ -1 +1 @@
1
- {"version":3,"file":"tool-call-buffer.d.ts","sourceRoot":"","sources":["../src/tool-call-buffer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAiB;IAC5C,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,WAAW,CAAS;IAE5B,IAAI,UAAU,IAAI,OAAO,CAExB;IAED;;;;;OAKG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE;IA8BhF,gDAAgD;IAChD,KAAK,IAAI,MAAM;CAKhB"}
1
+ {"version":3,"file":"tool-call-buffer.d.ts","sourceRoot":"","sources":["../src/tool-call-buffer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAajB;IACX,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,WAAW,CAAS;IAE5B,IAAI,UAAU,IAAI,OAAO,CAExB;IAED;;;;;OAKG;IACH,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAmC/E;IAED,gDAAgD;IAChD,KAAK,IAAI,MAAM,CAId;CACF"}
@@ -1,11 +1,24 @@
1
1
  /**
2
- * Buffers streaming text to detect and suppress `<tool_call>` tags. Text
2
+ * Buffers streaming text to detect and suppress model structural tags. Text
3
3
  * that cannot be part of a partial tag is released immediately; once a
4
- * full tag is seen, everything after it is suppressed until the stream
5
- * ends.
4
+ * full structural tag is seen, everything after it is suppressed until
5
+ * the stream ends.
6
6
  */
7
7
  export class ToolCallTagBuffer {
8
- static TAG = '<tool_call>';
8
+ static 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
+ ];
9
22
  pendingText = '';
10
23
  _suppressed = false;
11
24
  get suppressed() {
@@ -13,7 +26,7 @@ export class ToolCallTagBuffer {
13
26
  }
14
27
  /**
15
28
  * Feed text in. Returns `safeText` (emit as delta), `tagFound` (a full
16
- * `<tool_call>` was just seen), and `cleanPrefix` (text before the tag
29
+ * structural tag was just seen), and `cleanPrefix` (text before the tag
17
30
  * when `tagFound` — may contain whitespace; use `.trim()` only for
18
31
  * emptiness checks, never for emission).
19
32
  */
@@ -22,7 +35,13 @@ export class ToolCallTagBuffer {
22
35
  return { safeText: '', tagFound: false, cleanPrefix: '' };
23
36
  }
24
37
  this.pendingText += text;
25
- const tagIdx = this.pendingText.indexOf(ToolCallTagBuffer.TAG);
38
+ let tagIdx = -1;
39
+ for (const tag of ToolCallTagBuffer.TAGS) {
40
+ const idx = this.pendingText.indexOf(tag);
41
+ if (idx >= 0 && (tagIdx < 0 || idx < tagIdx)) {
42
+ tagIdx = idx;
43
+ }
44
+ }
26
45
  if (tagIdx >= 0) {
27
46
  const cleanPrefix = this.pendingText.slice(0, tagIdx);
28
47
  this._suppressed = true;
@@ -31,9 +50,10 @@ export class ToolCallTagBuffer {
31
50
  }
32
51
  // Hold back any suffix that could be the start of the tag.
33
52
  let safeLen = this.pendingText.length;
34
- for (let i = 1; i <= Math.min(this.pendingText.length, ToolCallTagBuffer.TAG.length - 1); i++) {
53
+ const maxTagLength = Math.max(...ToolCallTagBuffer.TAGS.map((tag) => tag.length));
54
+ for (let i = 1; i <= Math.min(this.pendingText.length, maxTagLength - 1); i++) {
35
55
  const suffix = this.pendingText.slice(-i);
36
- if (ToolCallTagBuffer.TAG.startsWith(suffix)) {
56
+ if (ToolCallTagBuffer.TAGS.some((tag) => tag.startsWith(suffix))) {
37
57
  safeLen = this.pendingText.length - i;
38
58
  break;
39
59
  }