@bendyline/gezel-inference 1.0.0 → 1.0.2
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/README.md +90 -8
- package/dist/cache/adapter.d.ts +7 -0
- package/dist/cache/controller.d.ts +12 -0
- package/dist/cache/controller.js +24 -0
- package/dist/catalog/chat-models/bt/btl4-35b-q4/manifest.json +1 -0
- package/dist/catalog/chat-models/gl/glm-5.2-754b-q2/manifest.json +1 -0
- package/dist/catalog/chat-models/index.json +1 -1
- package/dist/catalog/chat-models/lf/lfm2.5-24b-a2b-q4/manifest.json +122 -0
- package/dist/catalog/chat-models/lf/lfm2.5-24b-a2b-q4/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/chat-models/mi/minicpm-v4.7-35b-a3b-q4/manifest.json +185 -0
- package/dist/catalog/chat-models/mi/minicpm-v4.7-35b-a3b-q4/versions/1.0.0/manifest.json +94 -0
- package/dist/catalog/chat-models/mi/minicpm5-1b-q4/manifest.json +1 -0
- package/dist/catalog/chat-models/mi/minicpm5-2b-q4/manifest.json +1 -0
- package/dist/catalog/chat-models/qw/qwen3.5-0.8b-q4/manifest.json +1 -0
- package/dist/catalog/chat-models/raw-index.json +1 -1
- package/dist/catalog/provenance.json +1 -1
- package/dist/engines/electron-native-reuse.d.ts +9 -0
- package/dist/engines/electron-native-reuse.js +146 -45
- package/dist/engines/native-file-manifest.json +404 -404
- package/dist/engines/native-manifest.d.ts +11 -3
- package/dist/engines/native-manifest.js +48 -18
- package/dist/http/openai-compat/apple-model.d.ts +2 -0
- package/dist/http/openai-compat/apple-model.js +21 -1
- package/dist/http/openai-compat/chat-target.d.ts +12 -0
- package/dist/http/openai-compat/chat-target.js +27 -1
- package/dist/http/openai-compat/engine-failure.d.ts +25 -0
- package/dist/http/openai-compat/engine-failure.js +76 -0
- package/dist/http/openai-compat/responses-streaming.js +2 -1
- package/dist/http/openai-compat/responses-translate.d.ts +1 -1
- package/dist/http/openai-compat/translate.d.ts +1 -1
- package/dist/http/routes/engines.js +44 -3
- package/dist/http/routes/v1-chat.js +30 -8
- package/dist/http/routes/v1-models.d.ts +6 -0
- package/dist/http/routes/v1-models.js +137 -30
- package/dist/http/routes/v1-responses.js +16 -3
- package/dist/in-flight-requests.d.ts +19 -0
- package/dist/in-flight-requests.js +34 -0
- package/dist/inference-service.js +3 -3
- package/dist/knowledge/catalog-host.d.ts +9 -1
- package/dist/knowledge/catalog-host.js +2 -0
- package/dist/knowledge/manager.d.ts +14 -0
- package/dist/knowledge/manager.js +53 -6
- package/dist/knowledge/search-worker.d.ts +1 -1
- package/dist/knowledge/spatial-query.d.ts +2 -3
- package/dist/knowledge/spatial-query.js +12 -6
- package/dist/knowledge/worker-host.js +2 -0
- package/dist/memory/embed-core.d.ts +16 -0
- package/dist/memory/embed-core.js +31 -0
- package/dist/memory/embed-worker.d.ts +7 -3
- package/dist/memory/embed-worker.js +13 -4
- package/dist/memory/embeddings.d.ts +17 -2
- package/dist/memory/embeddings.js +36 -7
- package/dist/models/ensure.d.ts +2 -0
- package/dist/models/ensure.js +9 -0
- package/dist/perf/responsiveness.d.ts +14 -1
- package/dist/perf/responsiveness.js +77 -7
- package/dist/providers/llama-cpp/backend-health.d.ts +30 -0
- package/dist/providers/llama-cpp/backend-health.js +64 -0
- package/dist/providers/llama-cpp/backend-quarantine.d.ts +40 -0
- package/dist/providers/llama-cpp/backend-quarantine.js +100 -0
- package/dist/providers/llama-cpp/build-provider.js +140 -118
- package/dist/providers/llama-cpp/cache-adapter.d.ts +12 -0
- package/dist/providers/llama-cpp/cache-adapter.js +36 -1
- package/dist/providers/llama-cpp/devices.d.ts +11 -2
- package/dist/providers/llama-cpp/devices.js +10 -4
- package/dist/providers/llama-cpp/engine-flags.d.ts +0 -18
- package/dist/providers/llama-cpp/engine-flags.js +12 -18
- package/dist/providers/llama-cpp/gguf-metadata-async.js +7 -0
- package/dist/providers/llama-cpp/gguf-metadata.js +20 -3
- package/dist/providers/llama-cpp/gpu-startup-watch.d.ts +41 -0
- package/dist/providers/llama-cpp/gpu-startup-watch.js +58 -0
- package/dist/providers/llama-cpp/models.d.ts +6 -0
- package/dist/providers/llama-cpp/models.js +19 -1
- package/dist/providers/llama-cpp/offload-planner.d.ts +3 -18
- package/dist/providers/llama-cpp/offload-planner.js +3 -14
- package/dist/providers/llama-cpp/provider.d.ts +8 -0
- package/dist/providers/llama-cpp/provider.js +4 -1
- package/dist/providers/mlx/provider.js +14 -11
- package/dist/providers/mlx/python/batch_processors.py +26 -0
- package/dist/providers/mlx/python/gezel_mlx_server.py +88 -20
- package/dist/providers/mlx/python/qwen4_ple.py +9 -2
- package/dist/providers/mlx/python/response_format.py +374 -0
- package/dist/providers/mlx/python/spec_decode.py +5 -0
- package/dist/providers/mlx/python/think_budget.py +35 -1
- package/dist/providers/mlx/python/tool_grammar.py +62 -3
- package/dist/providers/mlx/reasoning-stream.d.ts +20 -2
- package/dist/providers/mlx/reasoning-stream.js +77 -16
- package/dist/providers/mlx/runtime-diagnostics.d.ts +16 -0
- package/dist/providers/mlx/runtime-diagnostics.js +29 -2
- package/dist/providers/native/capacity-broker.d.ts +51 -10
- package/dist/providers/native/capacity-broker.js +80 -9
- package/dist/providers/native/capacity-budget.d.ts +35 -3
- package/dist/providers/native/capacity-budget.js +49 -5
- package/dist/providers/native/device-capacity.d.ts +1 -1
- package/dist/providers/native/device-capacity.js +23 -5
- package/dist/providers/native/local-engine-runtime.js +15 -13
- package/dist/providers/native/measured-budget.js +2 -0
- package/dist/providers/native/supervisor.d.ts +25 -0
- package/dist/providers/native/supervisor.js +59 -7
- package/dist/remotes/identity.d.ts +1 -0
- package/dist/remotes/identity.js +6 -0
- package/dist/remotes/pinned-fetch.d.ts +2 -0
- package/dist/remotes/pinned-fetch.js +30 -1
- package/dist/remotes/types.d.ts +14 -0
- package/dist/runtime-lock.js +1 -1
- package/dist/system/amdgpu-adapters.d.ts +18 -0
- package/dist/system/amdgpu-adapters.js +55 -0
- package/dist/system/gpu-carve-out.d.ts +84 -0
- package/dist/system/gpu-carve-out.js +111 -0
- package/dist/system/memory.d.ts +17 -1
- package/dist/system/memory.js +105 -14
- package/dist/transformers-cache.d.ts +2 -0
- package/dist/transformers-cache.js +6 -3
- package/dist/utils/service-worker-entry.d.ts +4 -0
- package/dist/utils/service-worker-entry.js +4 -0
- package/package.json +14 -10
|
@@ -96,8 +96,15 @@ class SafeToolGrammarProcessor:
|
|
|
96
96
|
salvage layer is still there as the post-hoc safety net.
|
|
97
97
|
"""
|
|
98
98
|
|
|
99
|
-
def __init__(
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
inner: Any,
|
|
102
|
+
label: str = "tool-grammar",
|
|
103
|
+
fallback: str = "unconstrained + TS salvage",
|
|
104
|
+
) -> None:
|
|
100
105
|
self._inner = inner
|
|
106
|
+
self._label = label
|
|
107
|
+
self._fallback = fallback
|
|
101
108
|
self.disabled = False
|
|
102
109
|
|
|
103
110
|
def __call__(self, input_ids: Any, logits: Any) -> Any:
|
|
@@ -107,8 +114,8 @@ class SafeToolGrammarProcessor:
|
|
|
107
114
|
return self._inner(input_ids, logits)
|
|
108
115
|
except Exception as exc: # noqa: BLE001 - defensive: any matcher fault
|
|
109
116
|
print(
|
|
110
|
-
f"[
|
|
111
|
-
f"this turn (falling back to
|
|
117
|
+
f"[{self._label}] matcher error; disabling grammar for the rest of "
|
|
118
|
+
f"this turn (falling back to {self._fallback}): {exc}",
|
|
112
119
|
flush=True,
|
|
113
120
|
)
|
|
114
121
|
self.disabled = True
|
|
@@ -698,6 +705,58 @@ def build_grammar_string(
|
|
|
698
705
|
return None
|
|
699
706
|
|
|
700
707
|
|
|
708
|
+
# What each format's own grammar opens with, and how its reasoning block
|
|
709
|
+
# reads, for splicing an answer branch in front of the format's call rules.
|
|
710
|
+
_FORMAT_HEADS = {"hermes": _HERMES_HEAD, "gemma": _GEMMA_HEAD, "glm": _GLM_HEAD}
|
|
711
|
+
_THINK_BLOCKS = {
|
|
712
|
+
"hermes": "<think> TEXT </think>",
|
|
713
|
+
"glm": "<think> TEXT </think>",
|
|
714
|
+
"gemma": "<|channel> TEXT <channel|>",
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def build_answer_or_calls_grammar(
|
|
719
|
+
tools: Optional[List[Dict[str, Any]]],
|
|
720
|
+
hint: Dict[str, Any],
|
|
721
|
+
answer_schema: Dict[str, Any],
|
|
722
|
+
allow_think: bool = True,
|
|
723
|
+
) -> Optional[str]:
|
|
724
|
+
"""One grammar for a request with both tools and a response format.
|
|
725
|
+
|
|
726
|
+
OpenAI's contract: the model may call tools, and when it answers, the
|
|
727
|
+
answer follows the schema. So the reply is optional reasoning, then
|
|
728
|
+
either one or more calls in the format's own syntax (with this module's
|
|
729
|
+
name and key constraints) or a JSON value matching `answer_schema`.
|
|
730
|
+
Stacking the plain JSON grammar on the tool grammar instead left only
|
|
731
|
+
their intersection, JSON, which no call can satisfy. Prose outside the
|
|
732
|
+
reasoning is ruled out, as it is for a plain response format.
|
|
733
|
+
|
|
734
|
+
None when the format has no grammar here; the caller then keeps tool
|
|
735
|
+
calls free and leaves the answer unconstrained, as before.
|
|
736
|
+
|
|
737
|
+
`allow_think` False drops the reasoning block for a request that turned
|
|
738
|
+
thinking off: with prose masked, Gemma took the open `<|channel>` as its
|
|
739
|
+
way out and reasoned until max_tokens (2026-10-14).
|
|
740
|
+
"""
|
|
741
|
+
fmt = str((hint or {}).get("format") or "").strip()
|
|
742
|
+
head = _FORMAT_HEADS.get(fmt)
|
|
743
|
+
base = build_grammar_string(tools, hint)
|
|
744
|
+
if head is None or base is None or not base.startswith(head):
|
|
745
|
+
return None
|
|
746
|
+
schema = dict(answer_schema)
|
|
747
|
+
# Whitespace stays free inside the answer; see response_format.py.
|
|
748
|
+
schema["x-guidance"] = {**schema.get("x-guidance", {}), "whitespace_flexible": True}
|
|
749
|
+
return (
|
|
750
|
+
"%llguidance {}\n"
|
|
751
|
+
+ ("start: think? WS? (calls | answer) WS?\n" if allow_think else "start: WS? (calls | answer) WS?\n")
|
|
752
|
+
+ (f"think: {_THINK_BLOCKS[fmt]}\n" if allow_think else "")
|
|
753
|
+
+ "calls: tool_call (WS? tool_call)*\n"
|
|
754
|
+
+ f"answer: %json {json.dumps(schema)}\n"
|
|
755
|
+
+ "WS: /[ \\t\\r\\n]+/\n"
|
|
756
|
+
+ "TEXT: /(.|\\n)*/\n"
|
|
757
|
+
) + base[len(head) :]
|
|
758
|
+
|
|
759
|
+
|
|
701
760
|
def build_tool_grammar_processor(
|
|
702
761
|
tokenizer: Any,
|
|
703
762
|
tools: Optional[List[Dict[str, Any]]],
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/** Response header in which the sidecar reports the rendered prompt's state. */
|
|
2
|
+
export declare const REASONING_OPEN_HEADER = "x-gezel-reasoning-open";
|
|
3
|
+
/**
|
|
4
|
+
* Whether the engine says this turn's prompt left the model inside a reasoning
|
|
5
|
+
* block. The sidecar reads the rendered prompt, so this beats anything derived
|
|
6
|
+
* from the template text: Qwen 3.5's small models open `<think>` only when
|
|
7
|
+
* `enable_thinking` is explicitly true, and Gemma 4 opens its thought channel
|
|
8
|
+
* only after a tool response. Undefined when the header is absent.
|
|
9
|
+
*/
|
|
10
|
+
export declare function engineOpensReasoning(header: string | null | undefined): boolean | undefined;
|
|
1
11
|
/**
|
|
2
12
|
* How a chat template's generation prompt leaves the model inside a reasoning
|
|
3
13
|
* block:
|
|
@@ -11,7 +21,9 @@ export type TemplateReasoningOpen = 'always' | 'unless-thinking-off';
|
|
|
11
21
|
/**
|
|
12
22
|
* Whether the model's chat template opens a reasoning block as the last thing
|
|
13
23
|
* it emits before the model starts generating, and whether that depends on
|
|
14
|
-
* `enable_thinking`.
|
|
24
|
+
* `enable_thinking`. A fallback for a sidecar that does not report
|
|
25
|
+
* {@link REASONING_OPEN_HEADER}: a template guess cannot see which branch the
|
|
26
|
+
* request takes.
|
|
15
27
|
*
|
|
16
28
|
* Qwen-family templates end their generation prompt with a bare `<think>`, so
|
|
17
29
|
* the model's output begins *inside* reasoning and the only tag that ever
|
|
@@ -66,6 +78,11 @@ export interface ReasoningSplitChunk {
|
|
|
66
78
|
export declare class StreamingReasoningSplit {
|
|
67
79
|
private buffer;
|
|
68
80
|
private inReasoning;
|
|
81
|
+
/** A channel opener names its channel (`thought`) before the reasoning starts. */
|
|
82
|
+
private channelNamePending;
|
|
83
|
+
/** The blank lines a model leaves after closing its reasoning are not reply. */
|
|
84
|
+
private afterClose;
|
|
85
|
+
private sawVisible;
|
|
69
86
|
private readonly enabled;
|
|
70
87
|
constructor(opts: {
|
|
71
88
|
opensInReasoning: boolean;
|
|
@@ -79,7 +96,8 @@ export declare class StreamingReasoningSplit {
|
|
|
79
96
|
*/
|
|
80
97
|
flush(): ReasoningSplitChunk;
|
|
81
98
|
private drain;
|
|
82
|
-
private
|
|
99
|
+
private out;
|
|
100
|
+
private firstMarker;
|
|
83
101
|
/** Length of the trailing fragment that could still be a partial marker. */
|
|
84
102
|
private holdBackLength;
|
|
85
103
|
}
|
|
@@ -3,17 +3,39 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { createLogger } from '@bendyline/gezel';
|
|
4
4
|
const log = createLogger('mlx');
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Markers that open and close a reasoning block. Kept in sync with the paired
|
|
7
|
+
* forms `extractReasoning` strips at end of turn — this is the streaming half
|
|
8
|
+
* of the same knowledge. Gemma 4 opens a named channel (`<|channel>thought\n`,
|
|
9
|
+
* sometimes without the leading pipe) and closes it with `<channel|>`.
|
|
9
10
|
*/
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const
|
|
11
|
+
const OPEN_MARKERS = ['<think>', '<reasoning>', '[THINK]', '<|channel>', '<channel>'];
|
|
12
|
+
const CLOSE_MARKERS = ['</think>', '</reasoning>', '[/THINK]', '<|end|>', '<channel|>'];
|
|
13
|
+
const OPEN_PATTERN = /<think>|<reasoning>|\[THINK\]|<\|?channel>/i;
|
|
14
|
+
const CHANNEL_NAME = /^[A-Za-z_][\w-]*/;
|
|
15
|
+
/** The longest marker, so `push` knows how much tail to hold back. */
|
|
16
|
+
const MAX_MARKER_LENGTH = Math.max(...[...OPEN_MARKERS, ...CLOSE_MARKERS].map((marker) => marker.length));
|
|
17
|
+
/** Response header in which the sidecar reports the rendered prompt's state. */
|
|
18
|
+
export const REASONING_OPEN_HEADER = 'x-gezel-reasoning-open';
|
|
19
|
+
/**
|
|
20
|
+
* Whether the engine says this turn's prompt left the model inside a reasoning
|
|
21
|
+
* block. The sidecar reads the rendered prompt, so this beats anything derived
|
|
22
|
+
* from the template text: Qwen 3.5's small models open `<think>` only when
|
|
23
|
+
* `enable_thinking` is explicitly true, and Gemma 4 opens its thought channel
|
|
24
|
+
* only after a tool response. Undefined when the header is absent.
|
|
25
|
+
*/
|
|
26
|
+
export function engineOpensReasoning(header) {
|
|
27
|
+
if (header === '1')
|
|
28
|
+
return true;
|
|
29
|
+
if (header === '0')
|
|
30
|
+
return false;
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
13
33
|
/**
|
|
14
34
|
* Whether the model's chat template opens a reasoning block as the last thing
|
|
15
35
|
* it emits before the model starts generating, and whether that depends on
|
|
16
|
-
* `enable_thinking`.
|
|
36
|
+
* `enable_thinking`. A fallback for a sidecar that does not report
|
|
37
|
+
* {@link REASONING_OPEN_HEADER}: a template guess cannot see which branch the
|
|
38
|
+
* request takes.
|
|
17
39
|
*
|
|
18
40
|
* Qwen-family templates end their generation prompt with a bare `<think>`, so
|
|
19
41
|
* the model's output begins *inside* reasoning and the only tag that ever
|
|
@@ -108,6 +130,11 @@ export function requestOpensReasoning(templateOpen, body) {
|
|
|
108
130
|
export class StreamingReasoningSplit {
|
|
109
131
|
buffer = '';
|
|
110
132
|
inReasoning;
|
|
133
|
+
/** A channel opener names its channel (`thought`) before the reasoning starts. */
|
|
134
|
+
channelNamePending = false;
|
|
135
|
+
/** The blank lines a model leaves after closing its reasoning are not reply. */
|
|
136
|
+
afterClose = false;
|
|
137
|
+
sawVisible = false;
|
|
111
138
|
enabled;
|
|
112
139
|
constructor(opts) {
|
|
113
140
|
this.enabled = opts.enabled ?? true;
|
|
@@ -134,20 +161,48 @@ export class StreamingReasoningSplit {
|
|
|
134
161
|
let reasoning = '';
|
|
135
162
|
while (this.buffer.length > 0) {
|
|
136
163
|
if (this.inReasoning) {
|
|
137
|
-
|
|
164
|
+
if (this.channelNamePending) {
|
|
165
|
+
const name = CHANNEL_NAME.exec(this.buffer)?.[0] ?? '';
|
|
166
|
+
if (!final && name.length === this.buffer.length)
|
|
167
|
+
break;
|
|
168
|
+
this.buffer = this.buffer.slice(name.length).replace(/^[ \t]*\r?\n/, '');
|
|
169
|
+
this.channelNamePending = false;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const close = this.firstMarker(CLOSE_MARKERS);
|
|
138
173
|
if (!close)
|
|
139
174
|
break;
|
|
140
175
|
reasoning += this.buffer.slice(0, close.index);
|
|
141
176
|
this.buffer = this.buffer.slice(close.index + close.marker.length);
|
|
142
177
|
this.inReasoning = false;
|
|
178
|
+
this.afterClose = true;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (this.afterClose) {
|
|
182
|
+
if (!this.sawVisible && visible.trim() === '') {
|
|
183
|
+
visible = '';
|
|
184
|
+
this.buffer = this.buffer.replace(/^\s+/, '');
|
|
185
|
+
if (this.buffer.length === 0)
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
this.afterClose = false;
|
|
189
|
+
}
|
|
190
|
+
const open = OPEN_PATTERN.exec(this.buffer);
|
|
191
|
+
// A closer with no opener is a template-opened block the engine did not
|
|
192
|
+
// report; the text before it has already streamed, but the tag need not.
|
|
193
|
+
const stray = this.firstMarker(CLOSE_MARKERS);
|
|
194
|
+
if (stray && (!open || stray.index < open.index)) {
|
|
195
|
+
visible += this.buffer.slice(0, stray.index);
|
|
196
|
+
this.buffer = this.buffer.slice(stray.index + stray.marker.length);
|
|
197
|
+
this.afterClose = true;
|
|
143
198
|
continue;
|
|
144
199
|
}
|
|
145
|
-
const open = /<think>|<reasoning>|\[THINK\]/i.exec(this.buffer);
|
|
146
200
|
if (!open)
|
|
147
201
|
break;
|
|
148
202
|
visible += this.buffer.slice(0, open.index);
|
|
149
203
|
this.buffer = this.buffer.slice(open.index + open[0].length);
|
|
150
204
|
this.inReasoning = true;
|
|
205
|
+
this.channelNamePending = /channel/i.test(open[0]);
|
|
151
206
|
}
|
|
152
207
|
if (final) {
|
|
153
208
|
if (this.inReasoning)
|
|
@@ -155,8 +210,10 @@ export class StreamingReasoningSplit {
|
|
|
155
210
|
else
|
|
156
211
|
visible += this.buffer;
|
|
157
212
|
this.buffer = '';
|
|
158
|
-
return
|
|
213
|
+
return this.out(visible, reasoning);
|
|
159
214
|
}
|
|
215
|
+
if (this.channelNamePending)
|
|
216
|
+
return this.out(visible, reasoning);
|
|
160
217
|
// Hold back a tail that could still grow into a marker; release the rest.
|
|
161
218
|
const safeLength = this.buffer.length - this.holdBackLength();
|
|
162
219
|
if (safeLength > 0) {
|
|
@@ -167,12 +224,18 @@ export class StreamingReasoningSplit {
|
|
|
167
224
|
else
|
|
168
225
|
visible += released;
|
|
169
226
|
}
|
|
227
|
+
return this.out(visible, reasoning);
|
|
228
|
+
}
|
|
229
|
+
out(visible, reasoning) {
|
|
230
|
+
if (visible.trim() !== '')
|
|
231
|
+
this.sawVisible = true;
|
|
170
232
|
return { visible, reasoning };
|
|
171
233
|
}
|
|
172
|
-
|
|
234
|
+
firstMarker(markers) {
|
|
235
|
+
const haystack = this.buffer.toLowerCase();
|
|
173
236
|
let best = null;
|
|
174
|
-
for (const marker of
|
|
175
|
-
const index =
|
|
237
|
+
for (const marker of markers) {
|
|
238
|
+
const index = haystack.indexOf(marker.toLowerCase());
|
|
176
239
|
if (index === -1)
|
|
177
240
|
continue;
|
|
178
241
|
if (!best || index < best.index)
|
|
@@ -182,12 +245,10 @@ export class StreamingReasoningSplit {
|
|
|
182
245
|
}
|
|
183
246
|
/** Length of the trailing fragment that could still be a partial marker. */
|
|
184
247
|
holdBackLength() {
|
|
248
|
+
const candidates = (this.inReasoning ? CLOSE_MARKERS : [...OPEN_MARKERS, ...CLOSE_MARKERS]).map((marker) => marker.toLowerCase());
|
|
185
249
|
const maxTail = Math.min(MAX_MARKER_LENGTH - 1, this.buffer.length);
|
|
186
250
|
for (let length = maxTail; length > 0; length -= 1) {
|
|
187
251
|
const tail = this.buffer.slice(this.buffer.length - length).toLowerCase();
|
|
188
|
-
const candidates = this.inReasoning
|
|
189
|
-
? CLOSE_MARKERS.map((marker) => marker.toLowerCase())
|
|
190
|
-
: ['<think>', '<reasoning>', '[think]'];
|
|
191
252
|
if (candidates.some((marker) => marker.startsWith(tail)))
|
|
192
253
|
return length;
|
|
193
254
|
}
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
/** Read a process's resident-set size (bytes) via `ps` for engine telemetry. */
|
|
2
2
|
export declare function readProcessRssBytes(pid: number): Promise<number | null>;
|
|
3
|
+
/**
|
|
4
|
+
* The engine refused the request as the caller's to fix: a `response_format`
|
|
5
|
+
* schema its JSON grammar cannot enforce. Routes answer it with a 400
|
|
6
|
+
* `invalid_request_error`. Reported as a 500 `server_error`, it was retried
|
|
7
|
+
* by OpenAI SDKs although no retry could succeed.
|
|
8
|
+
*/
|
|
9
|
+
export declare class UnsupportedResponseFormatError extends Error {
|
|
10
|
+
readonly code = "unsupported_response_format";
|
|
11
|
+
constructor(message: string);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The error a failed engine response becomes. The sidecar answers 422 with a
|
|
15
|
+
* `response_format` detail only for a schema it refused (response_format.py);
|
|
16
|
+
* its other 422s are about images, and stay engine errors.
|
|
17
|
+
*/
|
|
18
|
+
export declare function mlxHttpError(status: number, statusText: string, body: string): Error;
|
|
3
19
|
/** Translate known mlx_vlm response shapes into user-actionable errors. */
|
|
4
20
|
export declare function translateMlxHttpError(status: number, statusText: string, body: string): string;
|
|
5
21
|
/** Detect an engine-side HTTP response stream closing mid-flight. */
|
|
@@ -27,6 +27,33 @@ export async function readProcessRssBytes(pid) {
|
|
|
27
27
|
}
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* The engine refused the request as the caller's to fix: a `response_format`
|
|
32
|
+
* schema its JSON grammar cannot enforce. Routes answer it with a 400
|
|
33
|
+
* `invalid_request_error`. Reported as a 500 `server_error`, it was retried
|
|
34
|
+
* by OpenAI SDKs although no retry could succeed.
|
|
35
|
+
*/
|
|
36
|
+
export class UnsupportedResponseFormatError extends Error {
|
|
37
|
+
code = 'unsupported_response_format';
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'UnsupportedResponseFormatError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The error a failed engine response becomes. The sidecar answers 422 with a
|
|
45
|
+
* `response_format` detail only for a schema it refused (response_format.py);
|
|
46
|
+
* its other 422s are about images, and stay engine errors.
|
|
47
|
+
*/
|
|
48
|
+
export function mlxHttpError(status, statusText, body) {
|
|
49
|
+
if (status === 422) {
|
|
50
|
+
const detail = tryParseJsonDetail(body, 1_000);
|
|
51
|
+
if (detail && /response[ _]format/i.test(detail)) {
|
|
52
|
+
return new UnsupportedResponseFormatError(detail);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return new Error(translateMlxHttpError(status, statusText, body));
|
|
56
|
+
}
|
|
30
57
|
/** Translate known mlx_vlm response shapes into user-actionable errors. */
|
|
31
58
|
export function translateMlxHttpError(status, statusText, body) {
|
|
32
59
|
if (/Repository Not Found for url/i.test(body) || /401 Client Error/i.test(body)) {
|
|
@@ -148,11 +175,11 @@ export function computePreFirstByteBudgetMs(approxPromptTokens) {
|
|
|
148
175
|
const scaled = PRE_FIRST_BYTE_BASE_MS + (over / 1000) * PRE_FIRST_BYTE_MS_PER_1K_TOKENS;
|
|
149
176
|
return Math.min(PRE_FIRST_BYTE_CAP_MS, Math.round(scaled));
|
|
150
177
|
}
|
|
151
|
-
function tryParseJsonDetail(body) {
|
|
178
|
+
function tryParseJsonDetail(body, maxChars = 200) {
|
|
152
179
|
try {
|
|
153
180
|
const obj = JSON.parse(body);
|
|
154
181
|
if (obj && typeof obj === 'object' && 'detail' in obj && typeof obj.detail === 'string') {
|
|
155
|
-
return obj.detail.slice(0,
|
|
182
|
+
return obj.detail.slice(0, maxChars);
|
|
156
183
|
}
|
|
157
184
|
}
|
|
158
185
|
catch {
|
|
@@ -46,6 +46,14 @@ export interface CapacityBrokerOptions {
|
|
|
46
46
|
* {@link computeCapacityBudget}. Omit to read the last published probe.
|
|
47
47
|
*/
|
|
48
48
|
gpuVramBytes?: number | null;
|
|
49
|
+
/**
|
|
50
|
+
* An integrated GPU's firmware carve-out — see
|
|
51
|
+
* {@link CapacityBudgetInput.reservedGpuBytes}. Travels with
|
|
52
|
+
* `gpuVramBytes`/`unifiedMemory` as one description of the host.
|
|
53
|
+
*/
|
|
54
|
+
reservedGpuBytes?: number | null;
|
|
55
|
+
/** What an integrated GPU can reach — see {@link CapacityBudgetInput.gpuAddressableBytes}. */
|
|
56
|
+
gpuAddressableBytes?: number | null;
|
|
49
57
|
/**
|
|
50
58
|
* May co-resident models spill into system RAM? `null`/omitted follows
|
|
51
59
|
* {@link autoAllowRamSpillover} for this host. See
|
|
@@ -192,6 +200,8 @@ export declare class CapacityBroker {
|
|
|
192
200
|
private readonly systemRamBytes;
|
|
193
201
|
private readonly unifiedMemory;
|
|
194
202
|
private readonly gpuVramBytes;
|
|
203
|
+
private readonly reservedGpuBytes;
|
|
204
|
+
private readonly gpuAddressableBytes;
|
|
195
205
|
private explicitAllowRamSpillover;
|
|
196
206
|
private readonly reservations;
|
|
197
207
|
constructor(opts?: CapacityBrokerOptions);
|
|
@@ -502,6 +512,32 @@ export declare function liveLaunchCapBytes(input: {
|
|
|
502
512
|
export declare const MIN_VIABLE_LOCAL_CONTEXT_TOKENS = 65536;
|
|
503
513
|
/** The same minimum on a host that has to trade context for memory. */
|
|
504
514
|
export declare const MIN_VIABLE_CONSTRAINED_CONTEXT_TOKENS = 32768;
|
|
515
|
+
/**
|
|
516
|
+
* The most KV cache adaptive growth plans for one llama.cpp engine. An
|
|
517
|
+
* explicit numeric context is the operator's call and skips growth
|
|
518
|
+
* altogether, and the ceiling never shortens the ladder-settled base window.
|
|
519
|
+
*
|
|
520
|
+
* Past this a bigger cache buys nothing: llama.cpp's prefill and decode cost
|
|
521
|
+
* follows the tokens actually present, not the reservation, so the rest only
|
|
522
|
+
* defers compaction. Across the 2026-10-12 scorecard (DGX Spark, n=3 per
|
|
523
|
+
* scenario) gemma4-31b never passed 41.7K tokens of an 82,944-token window
|
|
524
|
+
* it held as a 69.6 GB `--swa-full` cache, and qwen3.8-27b peaked at 94.8K,
|
|
525
|
+
* about 6 GB of its KV. That memory is worth more as a second resident
|
|
526
|
+
* model, and the same week two daemons each grew into one unified pool and
|
|
527
|
+
* hung the machine.
|
|
528
|
+
*/
|
|
529
|
+
export declare const AUTO_KV_CACHE_MAX_BYTES: number;
|
|
530
|
+
/** {@link AUTO_KV_CACHE_MAX_BYTES}, unless `GEZEL_LLAMA_AUTO_KV_MAX_GB` overrides it. */
|
|
531
|
+
export declare function autoKvCacheMaxBytes(): number;
|
|
532
|
+
/**
|
|
533
|
+
* The share of a shared pool's live free memory, left after the un-grown
|
|
534
|
+
* launch, that adaptive growth may spend. The rest stays free for whatever
|
|
535
|
+
* starts next: the desktop's own apps, a second model, another daemon. The
|
|
536
|
+
* base grant already holds the practical window; growth is the extra, and an
|
|
537
|
+
* extra that takes everything down to the OS reserve leaves the next tenant
|
|
538
|
+
* to page the machine.
|
|
539
|
+
*/
|
|
540
|
+
export declare const ADAPTIVE_GROWTH_SHARED_FREE_SHARE = 0.5;
|
|
505
541
|
/**
|
|
506
542
|
* The context floor THIS host is held to. A 16 GB Mac or an 8 GB card cannot
|
|
507
543
|
* back a 64K window next to the weights, and the admission ladder's only
|
|
@@ -622,10 +658,10 @@ export interface CtxMemoryClampInput {
|
|
|
622
658
|
*/
|
|
623
659
|
freeSystemRamBytes?: number;
|
|
624
660
|
/**
|
|
625
|
-
* Usable VRAM on a discrete card
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
661
|
+
* Usable VRAM on a discrete card. On a unified/shared-pool host it MUST be
|
|
662
|
+
* only an integrated GPU's firmware carve-out, which `freeSystemRamBytes`
|
|
663
|
+
* never sees; any "VRAM" that is the same physical RAM would double-count.
|
|
664
|
+
* `computeCapacityBudget` reports exactly that — pass its value.
|
|
629
665
|
*/
|
|
630
666
|
vramBytes: number;
|
|
631
667
|
minPerTurnCtxTokens?: number;
|
|
@@ -723,7 +759,7 @@ export interface AdaptiveCtxGrowthInput {
|
|
|
723
759
|
fastBudgetBytes: number;
|
|
724
760
|
committedOtherBytes?: number;
|
|
725
761
|
budgetKind: CapacityBudget['kind'];
|
|
726
|
-
/** Usable VRAM on a discrete card (`liveBudget.vramBytes`);
|
|
762
|
+
/** Usable VRAM on a discrete card (`liveBudget.vramBytes`); ignored elsewhere. */
|
|
727
763
|
vramBytes: number;
|
|
728
764
|
/**
|
|
729
765
|
* Graphics memory free right now on a discrete card, as device admission
|
|
@@ -747,6 +783,8 @@ export interface AdaptiveCtxGrowthInput {
|
|
|
747
783
|
* growth's to spend.
|
|
748
784
|
*/
|
|
749
785
|
allowSlotTrade?: boolean;
|
|
786
|
+
/** Test seam; defaults to {@link autoKvCacheMaxBytes}. */
|
|
787
|
+
maxKvCacheBytes?: number;
|
|
750
788
|
}
|
|
751
789
|
export interface AdaptiveCtxGrowthResult {
|
|
752
790
|
perTurnCtxTokens: number;
|
|
@@ -766,11 +804,9 @@ export interface AdaptiveCtxGrowthResult {
|
|
|
766
804
|
* settled slots and granted the un-grown request.
|
|
767
805
|
*
|
|
768
806
|
* Deliberately a second clamp pass rather than a bigger first request:
|
|
769
|
-
* the first ladder pass sizes slot ceilings
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
* `--swa-full` for models whose full cache fits at 64K and grow KV into
|
|
773
|
-
* system-RAM spill on discrete cards.
|
|
807
|
+
* the first ladder pass sizes slot ceilings and RAM-spillover admission,
|
|
808
|
+
* both of which must stay anchored on the practical 64K target — growing
|
|
809
|
+
* the request there would grow KV into system-RAM spill on discrete cards.
|
|
774
810
|
*
|
|
775
811
|
* Slot trade: when `allowSlotTrade` is set and the target is out of reach
|
|
776
812
|
* at the settled slot count, growth walks the count down — taking the
|
|
@@ -787,6 +823,11 @@ export interface AdaptiveCtxGrowthResult {
|
|
|
787
823
|
* the inputs match the base clamp. Inherits `VRAM_USABLE_FRACTION` and
|
|
788
824
|
* {@link LOCAL_ENGINE_COMPUTE_HEADROOM} through the shared clamp.
|
|
789
825
|
*
|
|
826
|
+
* Ceiling: total KV never grows past {@link autoKvCacheMaxBytes}, and on a
|
|
827
|
+
* shared pool with a live reading, never past the base KV plus
|
|
828
|
+
* {@link ADAPTIVE_GROWTH_SHARED_FREE_SHARE} of what the base launch leaves
|
|
829
|
+
* free. Both only stop growth; neither shrinks the base grant.
|
|
830
|
+
*
|
|
790
831
|
* MoE models never grow on a discrete card: `planMoeOffload` places routed
|
|
791
832
|
* experts around the KV reserve, and a grown reserve would silently evict
|
|
792
833
|
* experts from VRAM — trading decode speed for context behind the user's
|
|
@@ -151,12 +151,16 @@ export class CapacityBroker {
|
|
|
151
151
|
systemRamBytes;
|
|
152
152
|
unifiedMemory;
|
|
153
153
|
gpuVramBytes;
|
|
154
|
+
reservedGpuBytes;
|
|
155
|
+
gpuAddressableBytes;
|
|
154
156
|
explicitAllowRamSpillover;
|
|
155
157
|
reservations = new Map();
|
|
156
158
|
constructor(opts = {}) {
|
|
157
159
|
this.systemRamBytes = (opts.systemRamBytes ?? totalmem)();
|
|
158
160
|
this.unifiedMemory = opts.unifiedMemory;
|
|
159
161
|
this.gpuVramBytes = opts.gpuVramBytes;
|
|
162
|
+
this.reservedGpuBytes = opts.reservedGpuBytes;
|
|
163
|
+
this.gpuAddressableBytes = opts.gpuAddressableBytes;
|
|
160
164
|
this.explicitAllowRamSpillover = opts.allowRamSpillover ?? null;
|
|
161
165
|
this.explicitBudgetBytes = opts.budgetBytes ?? null;
|
|
162
166
|
// Assigned by applyBudget; the definite-assignment dance TS wants for
|
|
@@ -189,6 +193,12 @@ export class CapacityBroker {
|
|
|
189
193
|
: {
|
|
190
194
|
...(this.unifiedMemory === undefined ? {} : { unifiedMemory: this.unifiedMemory }),
|
|
191
195
|
...(this.gpuVramBytes === undefined ? {} : { gpuVramBytes: this.gpuVramBytes }),
|
|
196
|
+
...(this.reservedGpuBytes === undefined
|
|
197
|
+
? {}
|
|
198
|
+
: { reservedGpuBytes: this.reservedGpuBytes }),
|
|
199
|
+
...(this.gpuAddressableBytes === undefined
|
|
200
|
+
? {}
|
|
201
|
+
: { gpuAddressableBytes: this.gpuAddressableBytes }),
|
|
192
202
|
}),
|
|
193
203
|
});
|
|
194
204
|
}
|
|
@@ -713,6 +723,36 @@ function envGrowthCeilingTokens() {
|
|
|
713
723
|
const tokens = Number.parseInt(raw, 10);
|
|
714
724
|
return Number.isFinite(tokens) && tokens > 0 ? tokens : null;
|
|
715
725
|
}
|
|
726
|
+
/**
|
|
727
|
+
* The most KV cache adaptive growth plans for one llama.cpp engine. An
|
|
728
|
+
* explicit numeric context is the operator's call and skips growth
|
|
729
|
+
* altogether, and the ceiling never shortens the ladder-settled base window.
|
|
730
|
+
*
|
|
731
|
+
* Past this a bigger cache buys nothing: llama.cpp's prefill and decode cost
|
|
732
|
+
* follows the tokens actually present, not the reservation, so the rest only
|
|
733
|
+
* defers compaction. Across the 2026-10-12 scorecard (DGX Spark, n=3 per
|
|
734
|
+
* scenario) gemma4-31b never passed 41.7K tokens of an 82,944-token window
|
|
735
|
+
* it held as a 69.6 GB `--swa-full` cache, and qwen3.8-27b peaked at 94.8K,
|
|
736
|
+
* about 6 GB of its KV. That memory is worth more as a second resident
|
|
737
|
+
* model, and the same week two daemons each grew into one unified pool and
|
|
738
|
+
* hung the machine.
|
|
739
|
+
*/
|
|
740
|
+
export const AUTO_KV_CACHE_MAX_BYTES = 32 * GIB;
|
|
741
|
+
/** {@link AUTO_KV_CACHE_MAX_BYTES}, unless `GEZEL_LLAMA_AUTO_KV_MAX_GB` overrides it. */
|
|
742
|
+
export function autoKvCacheMaxBytes() {
|
|
743
|
+
const raw = process.env.GEZEL_LLAMA_AUTO_KV_MAX_GB;
|
|
744
|
+
const gb = raw ? Number.parseFloat(raw) : Number.NaN;
|
|
745
|
+
return Number.isFinite(gb) && gb > 0 ? gb * GIB : AUTO_KV_CACHE_MAX_BYTES;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* The share of a shared pool's live free memory, left after the un-grown
|
|
749
|
+
* launch, that adaptive growth may spend. The rest stays free for whatever
|
|
750
|
+
* starts next: the desktop's own apps, a second model, another daemon. The
|
|
751
|
+
* base grant already holds the practical window; growth is the extra, and an
|
|
752
|
+
* extra that takes everything down to the OS reserve leaves the next tenant
|
|
753
|
+
* to page the machine.
|
|
754
|
+
*/
|
|
755
|
+
export const ADAPTIVE_GROWTH_SHARED_FREE_SHARE = 0.5;
|
|
716
756
|
/**
|
|
717
757
|
* The context floor THIS host is held to. A 16 GB Mac or an 8 GB card cannot
|
|
718
758
|
* back a 64K window next to the weights, and the admission ladder's only
|
|
@@ -927,11 +967,9 @@ export const ADAPTIVE_GROWTH_TRADE_MIN_GAIN = 1.25;
|
|
|
927
967
|
* settled slots and granted the un-grown request.
|
|
928
968
|
*
|
|
929
969
|
* Deliberately a second clamp pass rather than a bigger first request:
|
|
930
|
-
* the first ladder pass sizes slot ceilings
|
|
931
|
-
*
|
|
932
|
-
*
|
|
933
|
-
* `--swa-full` for models whose full cache fits at 64K and grow KV into
|
|
934
|
-
* system-RAM spill on discrete cards.
|
|
970
|
+
* the first ladder pass sizes slot ceilings and RAM-spillover admission,
|
|
971
|
+
* both of which must stay anchored on the practical 64K target — growing
|
|
972
|
+
* the request there would grow KV into system-RAM spill on discrete cards.
|
|
935
973
|
*
|
|
936
974
|
* Slot trade: when `allowSlotTrade` is set and the target is out of reach
|
|
937
975
|
* at the settled slot count, growth walks the count down — taking the
|
|
@@ -948,6 +986,11 @@ export const ADAPTIVE_GROWTH_TRADE_MIN_GAIN = 1.25;
|
|
|
948
986
|
* the inputs match the base clamp. Inherits `VRAM_USABLE_FRACTION` and
|
|
949
987
|
* {@link LOCAL_ENGINE_COMPUTE_HEADROOM} through the shared clamp.
|
|
950
988
|
*
|
|
989
|
+
* Ceiling: total KV never grows past {@link autoKvCacheMaxBytes}, and on a
|
|
990
|
+
* shared pool with a live reading, never past the base KV plus
|
|
991
|
+
* {@link ADAPTIVE_GROWTH_SHARED_FREE_SHARE} of what the base launch leaves
|
|
992
|
+
* free. Both only stop growth; neither shrinks the base grant.
|
|
993
|
+
*
|
|
951
994
|
* MoE models never grow on a discrete card: `planMoeOffload` places routed
|
|
952
995
|
* experts around the KV reserve, and a grown reserve would silently evict
|
|
953
996
|
* experts from VRAM — trading decode speed for context behind the user's
|
|
@@ -974,6 +1017,27 @@ export function planAdaptiveContextGrowth(input) {
|
|
|
974
1017
|
? Math.max(0, input.measuredFreeVramBytes)
|
|
975
1018
|
: undefined;
|
|
976
1019
|
const cardBytes = measuredFreeVram !== undefined ? Math.min(input.vramBytes, measuredFreeVram) : input.vramBytes;
|
|
1020
|
+
const kvTotalAt = (slots, ctx) => slots * (kvFixedPerSlot + ctx * input.kvBytesPerToken);
|
|
1021
|
+
const baseKv = kvTotalAt(startSlots, base);
|
|
1022
|
+
const capKv = Math.max(0, input.maxKvCacheBytes ?? autoKvCacheMaxBytes());
|
|
1023
|
+
// Priced like the shared clamp (same OS reserve, same compute headroom) so
|
|
1024
|
+
// "what the base launch leaves free" means the same thing to both.
|
|
1025
|
+
const leftAfterBase = !discrete && input.freeSystemRamBytes !== undefined
|
|
1026
|
+
? Math.max(0, (input.freeSystemRamBytes -
|
|
1027
|
+
liveRamOsReserveBytes() -
|
|
1028
|
+
CTX_PLANNING_MARGIN_BYTES -
|
|
1029
|
+
input.weightsResidentBytes) *
|
|
1030
|
+
(1 - LOCAL_ENGINE_COMPUTE_HEADROOM) -
|
|
1031
|
+
baseKv)
|
|
1032
|
+
: undefined;
|
|
1033
|
+
const shareKv = leftAfterBase !== undefined
|
|
1034
|
+
? baseKv + leftAfterBase * ADAPTIVE_GROWTH_SHARED_FREE_SHARE
|
|
1035
|
+
: Number.POSITIVE_INFINITY;
|
|
1036
|
+
const kvCeiling = Math.min(capKv, shareKv);
|
|
1037
|
+
const ctxUnderCeiling = (slots) => {
|
|
1038
|
+
const perSlot = kvCeiling / slots - kvFixedPerSlot;
|
|
1039
|
+
return perSlot > 0 ? Math.floor(perSlot / input.kvBytesPerToken / 1024) * 1024 : 0;
|
|
1040
|
+
};
|
|
977
1041
|
const safeAt = (slots) => {
|
|
978
1042
|
const safe = clampCtxTokensForMemory({
|
|
979
1043
|
requestedPerTurnCtxTokens: target,
|
|
@@ -995,7 +1059,7 @@ export function planAdaptiveContextGrowth(input) {
|
|
|
995
1059
|
vramBytes: discrete ? cardBytes : 0,
|
|
996
1060
|
minPerTurnCtxTokens: 1,
|
|
997
1061
|
});
|
|
998
|
-
return Math.min(target, safe.perTurnCtxTokens);
|
|
1062
|
+
return Math.min(target, safe.perTurnCtxTokens, ctxUnderCeiling(slots));
|
|
999
1063
|
};
|
|
1000
1064
|
// Walk the slot count down only while the target stays out of reach:
|
|
1001
1065
|
// the first (largest) count that reaches it wins, else the floor count
|
|
@@ -1003,7 +1067,9 @@ export function planAdaptiveContextGrowth(input) {
|
|
|
1003
1067
|
const floorSlots = input.allowSlotTrade
|
|
1004
1068
|
? Math.min(startSlots, ADAPTIVE_GROWTH_MIN_SLOTS)
|
|
1005
1069
|
: startSlots;
|
|
1006
|
-
|
|
1070
|
+
// Floored at the base grant: when the base already sits above the KV
|
|
1071
|
+
// ceiling, the ceiling must not make a lane trade look like a big gain.
|
|
1072
|
+
const noTradeCtx = Math.max(base, safeAt(startSlots));
|
|
1007
1073
|
let chosenSlots = startSlots;
|
|
1008
1074
|
let chosenCtx = noTradeCtx;
|
|
1009
1075
|
for (let slots = startSlots - 1; slots >= floorSlots && chosenCtx < target; slots -= 1) {
|
|
@@ -1027,14 +1093,19 @@ export function planAdaptiveContextGrowth(input) {
|
|
|
1027
1093
|
const measuredNote = measuredFreeVram !== undefined && measuredFreeVram < Math.min(fastLeft, input.vramBytes)
|
|
1028
1094
|
? ` (capped at the ~${gb(measuredFreeVram)} of graphics memory other applications leave free)`
|
|
1029
1095
|
: '';
|
|
1096
|
+
const ceilingNote = grownTo < target && ctxUnderCeiling(chosenSlots) <= grownTo
|
|
1097
|
+
? kvCeiling === capKv
|
|
1098
|
+
? `; stopped at the ~${gb(capKv)} automatic KV ceiling`
|
|
1099
|
+
: `; stopped to keep ~${gb((leftAfterBase ?? 0) * (1 - ADAPTIVE_GROWTH_SHARED_FREE_SHARE))} free for other work`
|
|
1100
|
+
: '';
|
|
1030
1101
|
return {
|
|
1031
1102
|
perTurnCtxTokens: grownTo,
|
|
1032
1103
|
slots: chosenSlots,
|
|
1033
1104
|
grown: true,
|
|
1034
1105
|
reason: `growing context ${base} → ${grownTo} tokens/turn at ${chosenSlots} slot${chosenSlots === 1 ? '' : 's'}${slotNote} ` +
|
|
1035
1106
|
`(target ${target}): weights ~${gb(input.weightsResidentBytes)} + grown KV ` +
|
|
1036
|
-
`~${gb(chosenSlots
|
|
1037
|
-
`~${gb(fastLeft)}${measuredNote}`,
|
|
1107
|
+
`~${gb(kvTotalAt(chosenSlots, grownTo))} fits fast memory ` +
|
|
1108
|
+
`~${gb(fastLeft)}${measuredNote}${ceilingNote}`,
|
|
1038
1109
|
};
|
|
1039
1110
|
}
|
|
1040
1111
|
/**
|