@mlx-node/server 0.0.7 → 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.
- package/dist/chat-session-warm-reuse.d.ts +51 -0
- package/dist/chat-session-warm-reuse.d.ts.map +1 -0
- package/dist/chat-session-warm-reuse.js +68 -0
- package/dist/endpoints/messages-count-tokens.d.ts +8 -0
- package/dist/endpoints/messages-count-tokens.d.ts.map +1 -0
- package/dist/endpoints/messages-count-tokens.js +121 -0
- package/dist/endpoints/messages.d.ts +57 -5
- package/dist/endpoints/messages.d.ts.map +1 -1
- package/dist/endpoints/messages.js +1043 -147
- package/dist/endpoints/models.d.ts +2 -1
- package/dist/endpoints/models.d.ts.map +1 -1
- package/dist/endpoints/models.js +2 -2
- package/dist/endpoints/responses.d.ts +20 -7
- package/dist/endpoints/responses.d.ts.map +1 -1
- package/dist/endpoints/responses.js +572 -82
- package/dist/errors.d.ts +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -0
- package/dist/handler.d.ts +42 -0
- package/dist/handler.d.ts.map +1 -1
- package/dist/handler.js +6 -1
- package/dist/idle-sweeper.d.ts +245 -0
- package/dist/idle-sweeper.d.ts.map +1 -0
- package/dist/idle-sweeper.js +408 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/mappers/anthropic-request.d.ts +24 -2
- package/dist/mappers/anthropic-request.d.ts.map +1 -1
- package/dist/mappers/anthropic-request.js +222 -24
- package/dist/mappers/anthropic-response.d.ts +29 -4
- package/dist/mappers/anthropic-response.d.ts.map +1 -1
- package/dist/mappers/anthropic-response.js +143 -21
- package/dist/mappers/request.d.ts +48 -0
- package/dist/mappers/request.d.ts.map +1 -1
- package/dist/mappers/request.js +211 -35
- package/dist/mappers/response.d.ts.map +1 -1
- package/dist/mappers/response.js +13 -1
- package/dist/model-work-coordinator.d.ts +70 -0
- package/dist/model-work-coordinator.d.ts.map +1 -0
- package/dist/model-work-coordinator.js +120 -0
- package/dist/pending-writes.d.ts.map +1 -1
- package/dist/presets.d.ts +82 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +98 -0
- package/dist/registry.d.ts +31 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +33 -5
- package/dist/router.d.ts +4 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +34 -4
- package/dist/server.d.ts +76 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +48 -1
- package/dist/session-registry.d.ts +272 -18
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/session-registry.js +509 -37
- package/dist/stop-sequence-buffer.d.ts +58 -0
- package/dist/stop-sequence-buffer.d.ts.map +1 -0
- package/dist/stop-sequence-buffer.js +148 -0
- package/dist/text-recovery.d.ts +35 -0
- package/dist/text-recovery.d.ts.map +1 -0
- package/dist/text-recovery.js +41 -0
- package/dist/timing.d.ts +80 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +121 -0
- package/dist/tool-call-buffer.d.ts +5 -5
- package/dist/tool-call-buffer.d.ts.map +1 -1
- package/dist/tool-call-buffer.js +28 -8
- package/dist/types-anthropic.d.ts +161 -1
- package/dist/types-anthropic.d.ts.map +1 -1
- package/dist/types.d.ts +172 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -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
|
+
}
|
package/dist/timing.d.ts
ADDED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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,
|
|
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"}
|
package/dist/tool-call-buffer.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Buffers streaming text to detect and suppress
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
56
|
+
if (ToolCallTagBuffer.TAGS.some((tag) => tag.startsWith(suffix))) {
|
|
37
57
|
safeLen = this.pendingText.length - i;
|
|
38
58
|
break;
|
|
39
59
|
}
|