@bendyline/gezel-inference 1.0.6 → 1.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/cache/controller.d.ts +2 -0
- package/dist/cache/controller.js +38 -5
- package/dist/catalog/chat-models/index.json +1 -1
- package/dist/catalog/chat-models/lf/lfm3-1.7b-q4/manifest.json +114 -0
- package/dist/catalog/chat-models/lf/lfm3-1.7b-q4/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/chat-models/raw-index.json +1 -1
- package/dist/catalog/image-loras/index.json +1 -0
- package/dist/catalog/image-loras/kr/krea-2-ink-wash/manifest.json +31 -0
- package/dist/catalog/image-loras/kr/krea-2-ink-wash/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/kr/krea-2-isometric-3d/manifest.json +32 -0
- package/dist/catalog/image-loras/kr/krea-2-isometric-3d/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/kr/krea-2-kids-drawing/manifest.json +31 -0
- package/dist/catalog/image-loras/kr/krea-2-kids-drawing/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/kr/krea-2-pixel-art/manifest.json +31 -0
- package/dist/catalog/image-loras/kr/krea-2-pixel-art/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/kr/krea-2-soft-watercolor/manifest.json +31 -0
- package/dist/catalog/image-loras/kr/krea-2-soft-watercolor/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/kr/krea-2-turbo-4step/manifest.json +31 -0
- package/dist/catalog/image-loras/kr/krea-2-turbo-4step/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/mo/moss-image-gouache/manifest.json +29 -0
- package/dist/catalog/image-loras/mo/moss-image-gouache/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/mo/moss-image-sft-4step/manifest.json +30 -0
- package/dist/catalog/image-loras/mo/moss-image-sft-4step/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/qw/qwen-image-edit-2511-lightning/manifest.json +31 -0
- package/dist/catalog/image-loras/qw/qwen-image-edit-2511-lightning/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/raw-index.json +1 -0
- package/dist/catalog/image-loras/sd/sdxl-3d-render/manifest.json +29 -0
- package/dist/catalog/image-loras/sd/sdxl-3d-render/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/sd/sdxl-coloring-book/manifest.json +30 -0
- package/dist/catalog/image-loras/sd/sdxl-coloring-book/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/sd/sdxl-papercut/manifest.json +29 -0
- package/dist/catalog/image-loras/sd/sdxl-papercut/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/sd/sdxl-pixel-art/manifest.json +29 -0
- package/dist/catalog/image-loras/sd/sdxl-pixel-art/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-models/fl/flux-2-klein-4b-q4/manifest.json +3 -0
- package/dist/catalog/image-models/index.json +1 -1
- package/dist/catalog/image-models/qw/qwen-image-edit-2511-q4/manifest.json +44 -0
- package/dist/catalog/image-models/qw/qwen-image-edit-2511-q4/versions/1.0.0/manifest.json +36 -0
- package/dist/catalog/image-models/raw-index.json +1 -1
- package/dist/catalog/knowledge-catalogs/index.json +1 -1
- package/dist/catalog/knowledge-catalogs/raw-index.json +1 -1
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-arts/versions/2026.10.2/manifest.json +181 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-astronomy/versions/2026.10.2/manifest.json +181 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-biology/versions/2026.10.2/manifest.json +217 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-business/versions/2026.10.2/manifest.json +197 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-film-tv/versions/2026.10.2/manifest.json +197 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-food-drink/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-games/versions/2026.10.2/manifest.json +181 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-general/manifest.json +55 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-general/versions/2026.10.2/manifest.json +165 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-geography/versions/2026.10.2/manifest.json +217 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-history/versions/2026.10.2/manifest.json +217 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-language/versions/2026.10.2/manifest.json +181 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-law/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-literature/versions/2026.10.2/manifest.json +193 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-mathematics/versions/2026.10.2/manifest.json +189 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-medicine/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-military/versions/2026.10.2/manifest.json +189 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-music/versions/2026.10.2/manifest.json +197 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-arts/versions/2026.10.2/manifest.json +209 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-business/versions/2026.10.2/manifest.json +189 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-entertainment/versions/2026.10.2/manifest.json +201 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-military/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-other/versions/2026.10.2/manifest.json +165 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-politics/versions/2026.10.2/manifest.json +197 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-religion/versions/2026.10.2/manifest.json +201 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-science/versions/2026.10.2/manifest.json +205 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-sports/versions/2026.10.2/manifest.json +237 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-politics/versions/2026.10.2/manifest.json +197 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-religion-philosophy/versions/2026.10.2/manifest.json +189 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-science/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-sports/versions/2026.10.2/manifest.json +193 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-technology/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-transportation/versions/2026.10.2/manifest.json +189 -0
- package/dist/catalog/provenance.json +2 -1
- package/dist/engine-components.js +2 -2
- package/dist/engines/electron-native-reuse.d.ts +7 -1
- package/dist/engines/electron-native-reuse.js +12 -4
- package/dist/engines/native-file-manifest.json +546 -406
- package/dist/engines/native-manifest.d.ts +4 -4
- package/dist/engines/native-manifest.js +36 -36
- package/dist/engines/resolver.js +1 -0
- package/dist/http/auth-context.d.ts +12 -0
- package/dist/http/routes/image-ensure-routes.d.ts +4 -0
- package/dist/http/routes/image-ensure-routes.js +102 -0
- package/dist/http/routes/v1-image-styles.d.ts +5 -0
- package/dist/http/routes/v1-image-styles.js +91 -0
- package/dist/http/routes/v1-images.d.ts +3 -1
- package/dist/http/routes/v1-images.js +48 -99
- package/dist/inference-service.js +2 -1
- package/dist/knowledge/manager.d.ts +3 -27
- package/dist/knowledge/manager.js +8 -364
- package/dist/knowledge/unified-search.d.ts +61 -0
- package/dist/knowledge/unified-search.js +373 -0
- package/dist/knowledge/worker-host.js +2 -0
- package/dist/machine-engine/inspect-runtime.js +12 -3
- package/dist/memory/embed-core.js +3 -0
- package/dist/memory/embeddings.js +2 -0
- package/dist/models/image-fit.js +2 -2
- package/dist/models/weight-facts.d.ts +5 -0
- package/dist/models/weight-facts.js +5 -2
- package/dist/providers/ds4/build-provider.js +1 -1
- package/dist/providers/ds4/residency.js +3 -3
- package/dist/providers/image/img2img-support.d.ts +36 -1
- package/dist/providers/image/img2img-support.js +30 -1
- package/dist/providers/image/local-engine.d.ts +1 -0
- package/dist/providers/image/local-engine.js +8 -2
- package/dist/providers/image/local-factory.d.ts +10 -1
- package/dist/providers/image/local-factory.js +33 -6
- package/dist/providers/image/mock.d.ts +8 -1
- package/dist/providers/image/mock.js +46 -1
- package/dist/providers/image/pull-registry.d.ts +25 -3
- package/dist/providers/image/pull-registry.js +96 -34
- package/dist/providers/image/sd-cpp.d.ts +63 -4
- package/dist/providers/image/sd-cpp.js +368 -38
- package/dist/providers/image/types.d.ts +76 -4
- package/dist/providers/image/types.js +8 -0
- package/dist/providers/llama-cpp/build-provider.js +3 -2
- package/dist/providers/llama-cpp/cache-adapter.d.ts +3 -0
- package/dist/providers/llama-cpp/cache-adapter.js +18 -4
- package/dist/providers/llama-cpp/devices.js +2 -2
- package/dist/providers/llama-cpp/engine-flags.d.ts +5 -0
- package/dist/providers/llama-cpp/engine-flags.js +5 -2
- package/dist/providers/mlx/async-file-handoff.d.ts +9 -1
- package/dist/providers/mlx/async-file-handoff.js +32 -4
- package/dist/providers/mlx/provider.js +41 -20
- package/dist/providers/mlx/python/gezel_mlx_server.py +9 -2
- package/dist/providers/mlx/python/spec_decode.py +44 -5
- package/dist/providers/mlx/runtime-diagnostics.d.ts +6 -0
- package/dist/providers/mlx/runtime-diagnostics.js +18 -1
- package/dist/providers/mlx/tool-call-protocol.d.ts +4 -3
- package/dist/providers/mlx/tool-call-protocol.js +4 -3
- package/dist/providers/native/capacity-broker.d.ts +1 -1
- package/dist/providers/native/capacity-broker.js +8 -8
- package/dist/providers/native/capacity-budget.js +5 -4
- package/dist/providers/native/device-capacity-ledger.d.ts +5 -1
- package/dist/providers/native/device-capacity-ledger.js +60 -14
- package/dist/providers/native/device-capacity.js +4 -3
- package/dist/providers/native/local-engine-runtime.js +4 -4
- package/dist/providers/native/process-identity.d.ts +40 -0
- package/dist/providers/native/process-identity.js +156 -0
- package/dist/relevance/relevance-core.js +2 -0
- package/dist/relevance/relevance-model.js +2 -0
- package/dist/relevance/settings.js +2 -2
- package/dist/runtime-lock.d.ts +19 -5
- package/dist/runtime-lock.js +116 -26
- package/dist/system/host-memory.d.ts +46 -0
- package/dist/system/host-memory.js +89 -0
- package/dist/system/memory.js +5 -4
- package/dist/transformers-cache.js +2 -0
- package/dist/utils/download-common.d.ts +1 -1
- package/package.json +8 -8
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageAspectRatio, ImageModelAuxiliaryRole, ImageQuality } from '@bendyline/gezel';
|
|
1
|
+
import type { ImageAspectRatio, ImageLoraPrompt, ImageLoraPurpose, ImageLoraRender, ImageModelAuxiliaryRole, ImageQuality } from '@bendyline/gezel';
|
|
2
2
|
export type { ImageModelAuxiliaryRole } from '@bendyline/gezel';
|
|
3
3
|
/**
|
|
4
4
|
* ImageProvider — the service-side abstraction for local image generation.
|
|
@@ -69,12 +69,21 @@ export interface ImageGenerationInput {
|
|
|
69
69
|
seed?: number;
|
|
70
70
|
/**
|
|
71
71
|
* Source images for image editing / reference-driven generation.
|
|
72
|
-
* Cloud providers accept multiple;
|
|
73
|
-
*
|
|
72
|
+
* Cloud providers accept multiple; a local model that reads references
|
|
73
|
+
* takes up to its limit, and sd-cpp's img2img path uses the first only
|
|
74
|
+
* (the others are dropped with a warning).
|
|
74
75
|
*/
|
|
75
76
|
inputImages?: ImageInputBytes[];
|
|
76
|
-
/** Img2img strength (0 = keep source, 1 = ignore source). sd-cpp only. */
|
|
77
|
+
/** Img2img strength (0 = keep source, 1 = ignore source). sd-cpp img2img only. */
|
|
77
78
|
strength?: number;
|
|
79
|
+
/**
|
|
80
|
+
* LoRAs to apply, by catalog id: a style, a fast mode. The engine reads each
|
|
81
|
+
* one's file, strength, prompt words and render settings from its own
|
|
82
|
+
* install, so an engine that predates LoRAs renders correctly without them.
|
|
83
|
+
*/
|
|
84
|
+
loras?: Array<{
|
|
85
|
+
id: string;
|
|
86
|
+
}>;
|
|
78
87
|
/**
|
|
79
88
|
* Optional callback fired as the engine advances through sampling
|
|
80
89
|
* steps. Today only the local sd-cpp provider emits these; cloud
|
|
@@ -95,6 +104,11 @@ export declare class ImageGenerationCancelledError extends Error {
|
|
|
95
104
|
readonly code = "image_generation_cancelled";
|
|
96
105
|
constructor();
|
|
97
106
|
}
|
|
107
|
+
/** A model made only for editing was asked for a picture with nothing to edit. */
|
|
108
|
+
export declare class ImageSourceRequiredError extends Error {
|
|
109
|
+
readonly code = "image_source_required";
|
|
110
|
+
constructor(modelName: string);
|
|
111
|
+
}
|
|
98
112
|
/**
|
|
99
113
|
* The engine could not fit the render into graphics memory. sd-server plans
|
|
100
114
|
* placement at launch and can misjudge free memory held by another engine;
|
|
@@ -120,6 +134,11 @@ export interface ImageGenerationMeta {
|
|
|
120
134
|
img2imgSkippedReason?: string;
|
|
121
135
|
/** Provider limits or adjustments to the requested image settings. */
|
|
122
136
|
settingsNotes?: string[];
|
|
137
|
+
/** The LoRAs the engine applied; absent when none. */
|
|
138
|
+
loras?: Array<{
|
|
139
|
+
id: string;
|
|
140
|
+
name: string;
|
|
141
|
+
}>;
|
|
123
142
|
}
|
|
124
143
|
export interface ImageGenerationOutput {
|
|
125
144
|
png: Buffer;
|
|
@@ -134,6 +153,45 @@ export interface InstalledImageModelInfo {
|
|
|
134
153
|
weightsKind?: 'checkpoint' | 'diffusion-model';
|
|
135
154
|
/** Explicit catalog capability recorded at pull time; absent → resolve via the ladder. */
|
|
136
155
|
supportsImg2Img?: boolean;
|
|
156
|
+
/**
|
|
157
|
+
* Resolved editing capability (see `resolveImageEditCapability`), so a
|
|
158
|
+
* caller across the machine broker or a paired server can choose a model
|
|
159
|
+
* without the engine's assessment map.
|
|
160
|
+
*/
|
|
161
|
+
editMode?: 'reference' | 'img2img';
|
|
162
|
+
maxSourceImages?: number;
|
|
163
|
+
editOnly?: boolean;
|
|
164
|
+
/**
|
|
165
|
+
* Installed LoRAs that fit this model. Carried on the model listing because
|
|
166
|
+
* that listing already reaches the product daemon from a machine broker or
|
|
167
|
+
* a paired server; an engine that predates LoRAs lists none.
|
|
168
|
+
*/
|
|
169
|
+
loras?: InstalledImageLoraSummary[];
|
|
170
|
+
}
|
|
171
|
+
/** An installed LoRA as the model listing names it. */
|
|
172
|
+
export interface InstalledImageLoraSummary {
|
|
173
|
+
id: string;
|
|
174
|
+
name: string;
|
|
175
|
+
purpose: ImageLoraPurpose;
|
|
176
|
+
/** The steps a speed LoRA renders in. */
|
|
177
|
+
steps?: number;
|
|
178
|
+
}
|
|
179
|
+
export interface InstalledImageLoraInfo extends InstalledImageLoraSummary {
|
|
180
|
+
baseModels: string[];
|
|
181
|
+
approxSizeBytes: number;
|
|
182
|
+
installedAt: string;
|
|
183
|
+
}
|
|
184
|
+
/** What a LoRA install needs from its catalog entry; persisted beside the file. */
|
|
185
|
+
export interface ImageLoraPullSpec {
|
|
186
|
+
downloadUrl: string;
|
|
187
|
+
sha256: string;
|
|
188
|
+
approxSizeBytes: number;
|
|
189
|
+
name: string;
|
|
190
|
+
baseModels: string[];
|
|
191
|
+
purpose: ImageLoraPurpose;
|
|
192
|
+
strength: number;
|
|
193
|
+
prompt?: ImageLoraPrompt;
|
|
194
|
+
render?: ImageLoraRender;
|
|
137
195
|
}
|
|
138
196
|
export type ImageModelPullEvent = {
|
|
139
197
|
type: 'progress';
|
|
@@ -187,6 +245,12 @@ export interface ImageModelPullSpec {
|
|
|
187
245
|
* weights-kind default.
|
|
188
246
|
*/
|
|
189
247
|
supportsImg2Img?: boolean;
|
|
248
|
+
/** Catalog `referenceImages`, persisted the same way. */
|
|
249
|
+
referenceImages?: {
|
|
250
|
+
max: number;
|
|
251
|
+
};
|
|
252
|
+
/** Catalog `editOnly`, persisted the same way. */
|
|
253
|
+
editOnly?: boolean;
|
|
190
254
|
/** Auxiliary files (VAE, text encoders) downloaded alongside the unet. */
|
|
191
255
|
auxiliaryFiles: ImageModelAuxiliaryPullSpec[];
|
|
192
256
|
}
|
|
@@ -225,6 +289,14 @@ export interface ImageProvider {
|
|
|
225
289
|
*/
|
|
226
290
|
pullModel(id: string, spec: ImageModelPullSpec, signal?: AbortSignal): AsyncIterable<ImageModelPullEvent>;
|
|
227
291
|
deleteModel(id: string): Promise<void>;
|
|
292
|
+
/**
|
|
293
|
+
* LoRA management for an engine that applies them (the local sd-cpp
|
|
294
|
+
* engine). Rendering never needs these: the model listing carries the
|
|
295
|
+
* installed LoRAs and `generate` takes their ids.
|
|
296
|
+
*/
|
|
297
|
+
pullLora?(id: string, spec: ImageLoraPullSpec, signal?: AbortSignal): AsyncIterable<ImageModelPullEvent>;
|
|
298
|
+
listLoras?(): Promise<InstalledImageLoraInfo[]>;
|
|
299
|
+
deleteLora?(id: string): Promise<void>;
|
|
228
300
|
/**
|
|
229
301
|
* Probe the underlying engine for reachability without doing real
|
|
230
302
|
* work. Implementations should be fast (sub-second) and never throw
|
|
@@ -6,6 +6,14 @@ export class ImageGenerationCancelledError extends Error {
|
|
|
6
6
|
this.name = 'ImageGenerationCancelledError';
|
|
7
7
|
}
|
|
8
8
|
}
|
|
9
|
+
/** A model made only for editing was asked for a picture with nothing to edit. */
|
|
10
|
+
export class ImageSourceRequiredError extends Error {
|
|
11
|
+
code = 'image_source_required';
|
|
12
|
+
constructor(modelName) {
|
|
13
|
+
super(`${modelName} changes a picture you give it. Attach the picture to change, or pick a model that draws new ones.`);
|
|
14
|
+
this.name = 'ImageSourceRequiredError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
9
17
|
/**
|
|
10
18
|
* The engine could not fit the render into graphics memory. sd-server plans
|
|
11
19
|
* placement at launch and can misjudge free memory held by another engine;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { availableParallelism } from 'node:os';
|
|
2
1
|
import { delimiter, dirname, join } from 'node:path';
|
|
3
2
|
import { DEFAULT_LOCAL_ENGINE_IDLE_TIMEOUT_MS, createLogger, estimateLlamaCppResidentBytes, llamaCppBackendExclusion, llamaCppBackendLabel, } from '@bendyline/gezel';
|
|
4
3
|
import { LLAMA_QUARANTINE_STRIKES, clearLlamaStartupStrikes, recordLlamaStartupCrash, unrunnableBuildExit, } from '@bendyline/gezel/native';
|
|
@@ -6,6 +5,7 @@ import { gezelPaths } from '@bendyline/gezel/paths';
|
|
|
6
5
|
import { mmprojBudgetBytes, nativeVisionEnabledFor } from '../../chat/vision-capability.js';
|
|
7
6
|
import { effectiveEngineRelease, isEnginePinned } from '../../engines/native-manifest.js';
|
|
8
7
|
import { engineInstallPhase } from '../../engines/registry.js';
|
|
8
|
+
import { availableCpuCount, containerCpuLimit } from '../../system/host-memory.js';
|
|
9
9
|
import { localDeviceSettingsLabel, noModelYetMessage, notReadyError, } from '../active-install-message.js';
|
|
10
10
|
import { resolveCatalogLlamaCppEngineConfig, resolveCatalogReasoningBudget, } from '../catalog-model-config.js';
|
|
11
11
|
import { CapacityDeniedError, NOT_ENOUGH_MEMORY_MESSAGE, availableSystemRamBytes, formatContextCapacityDenial, minViableLocalContextTokens, planAdaptiveContextGrowth, pressureIdleGraceMs, resolveLlamaCppContextRequirement, } from '../native/capacity-broker.js';
|
|
@@ -1352,7 +1352,8 @@ export async function buildLlamaCppProvider(opts) {
|
|
|
1352
1352
|
reasoningFormat: process.env.GEZEL_LLAMA_REASONING_FORMAT?.trim() || undefined,
|
|
1353
1353
|
reasoningPreserve,
|
|
1354
1354
|
backend: llamaBuildMetadata?.backend ?? process.env.GEZEL_LLAMA_SERVER_BACKEND,
|
|
1355
|
-
hostCpuCount:
|
|
1355
|
+
hostCpuCount: availableCpuCount(),
|
|
1356
|
+
containerCpuLimit: containerCpuLimit() ?? undefined,
|
|
1356
1357
|
});
|
|
1357
1358
|
return {
|
|
1358
1359
|
command: binary,
|
|
@@ -201,6 +201,7 @@ export declare class LlamaCppCacheAdapter implements EngineCacheAdapter {
|
|
|
201
201
|
prepareForSend(sessionId: string, systemPrompt?: string, layers?: SystemPromptLayers): Promise<void>;
|
|
202
202
|
buildRequestExtras(sessionId: string): Record<string, unknown>;
|
|
203
203
|
evict(sessionIds: readonly string[]): Promise<void>;
|
|
204
|
+
private evictUnlocked;
|
|
204
205
|
markEphemeral(sessionId: string): void;
|
|
205
206
|
reportUsage(): Promise<readonly EngineCacheUsage[]>;
|
|
206
207
|
warm(_sessionId: string, _messages: readonly CacheWarmMessage[]): Promise<void>;
|
|
@@ -210,6 +211,7 @@ export declare class LlamaCppCacheAdapter implements EngineCacheAdapter {
|
|
|
210
211
|
* sessions for the next boot.
|
|
211
212
|
*/
|
|
212
213
|
flushAll(): Promise<number>;
|
|
214
|
+
private flushAllUnlocked;
|
|
213
215
|
/**
|
|
214
216
|
* Async path: allocate a slot for `sessionId`, save the LRU victim
|
|
215
217
|
* (if any) to disk, and try to restore the new slot from disk
|
|
@@ -217,6 +219,7 @@ export declare class LlamaCppCacheAdapter implements EngineCacheAdapter {
|
|
|
217
219
|
* {@link prepareForSend}.
|
|
218
220
|
*/
|
|
219
221
|
private allocateSlot;
|
|
222
|
+
private withAllocationLock;
|
|
220
223
|
private assertEngineGeneration;
|
|
221
224
|
private allocateSlotUnlocked;
|
|
222
225
|
/**
|
|
@@ -258,7 +258,11 @@ export class LlamaCppCacheAdapter {
|
|
|
258
258
|
};
|
|
259
259
|
}
|
|
260
260
|
async evict(sessionIds) {
|
|
261
|
-
|
|
261
|
+
return this.withAllocationLock((generation) => this.evictUnlocked(sessionIds, generation));
|
|
262
|
+
}
|
|
263
|
+
async evictUnlocked(sessionIds, generation) {
|
|
264
|
+
if (generation !== this.engineGeneration)
|
|
265
|
+
return;
|
|
262
266
|
// Save before forgetting so the session can resume on next access.
|
|
263
267
|
// Persistence is best-effort; failures are logged and the local
|
|
264
268
|
// mapping is still cleared (controller view stays consistent).
|
|
@@ -352,7 +356,11 @@ export class LlamaCppCacheAdapter {
|
|
|
352
356
|
async flushAll() {
|
|
353
357
|
if (!this.slotSavePath)
|
|
354
358
|
return 0;
|
|
355
|
-
|
|
359
|
+
return this.withAllocationLock((generation) => this.flushAllUnlocked(generation));
|
|
360
|
+
}
|
|
361
|
+
async flushAllUnlocked(generation) {
|
|
362
|
+
if (generation !== this.engineGeneration)
|
|
363
|
+
return 0;
|
|
356
364
|
let saved = 0;
|
|
357
365
|
for (const [sessionId, slot] of this.sessionToSlot) {
|
|
358
366
|
if (await this.saveSlotForSession(slot, sessionId))
|
|
@@ -369,6 +377,13 @@ export class LlamaCppCacheAdapter {
|
|
|
369
377
|
* {@link prepareForSend}.
|
|
370
378
|
*/
|
|
371
379
|
async allocateSlot(sessionId) {
|
|
380
|
+
return this.withAllocationLock((generation) => {
|
|
381
|
+
this.assertEngineGeneration(generation);
|
|
382
|
+
return this.allocateSlotUnlocked(sessionId, generation);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
// Eviction and idle flushing must not save a slot after a handoff has rebound it.
|
|
386
|
+
async withAllocationLock(work) {
|
|
372
387
|
const generation = this.engineGeneration;
|
|
373
388
|
const previous = this.allocationTail;
|
|
374
389
|
let release;
|
|
@@ -377,8 +392,7 @@ export class LlamaCppCacheAdapter {
|
|
|
377
392
|
});
|
|
378
393
|
await previous;
|
|
379
394
|
try {
|
|
380
|
-
|
|
381
|
-
return await this.allocateSlotUnlocked(sessionId, generation);
|
|
395
|
+
return await work(generation);
|
|
382
396
|
}
|
|
383
397
|
finally {
|
|
384
398
|
release();
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { execFile } from 'node:child_process';
|
|
19
19
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
20
|
-
import { totalmem } from 'node:os';
|
|
21
20
|
import { delimiter, dirname, join } from 'node:path';
|
|
22
21
|
import { promisify } from 'node:util';
|
|
23
22
|
import { windowsHeadlessSpawnOptions } from '@bendyline/gezel/native';
|
|
24
23
|
import { gezelHome } from '@bendyline/gezel/paths';
|
|
24
|
+
import { totalMemoryBytes } from '../../system/host-memory.js';
|
|
25
25
|
const execFileAsync = promisify(execFile);
|
|
26
26
|
const LINE_RE = /^\s*(\S+?):\s*(.+?)\s*\((\d+)\s*MiB,\s*(\d+)\s*MiB\s+free\)\s*$/;
|
|
27
27
|
/**
|
|
@@ -158,7 +158,7 @@ export function llamaBinaryFingerprint(binaryPath) {
|
|
|
158
158
|
export async function probeLlamaDevices(opts) {
|
|
159
159
|
const { binaryPath, home, force } = opts;
|
|
160
160
|
const now = opts.nowIso ?? (() => new Date().toISOString());
|
|
161
|
-
const systemRamBytes = opts.systemRamBytes ??
|
|
161
|
+
const systemRamBytes = opts.systemRamBytes ?? totalMemoryBytes();
|
|
162
162
|
const path = cachePath(home);
|
|
163
163
|
const binary = llamaBinaryFingerprint(binaryPath);
|
|
164
164
|
if (!force && existsSync(path)) {
|
|
@@ -133,6 +133,11 @@ export interface EngineFlagInput {
|
|
|
133
133
|
backend?: string | undefined;
|
|
134
134
|
/** Logical CPUs available to the daemon; caps the GPU-resident thread default. */
|
|
135
135
|
hostCpuCount?: number | undefined;
|
|
136
|
+
/**
|
|
137
|
+
* CPUs a container leaves the daemon, when fewer than the host has (see
|
|
138
|
+
* `containerCpuLimit`). Without it llama.cpp sizes its pool from the host.
|
|
139
|
+
*/
|
|
140
|
+
containerCpuLimit?: number | undefined;
|
|
136
141
|
}
|
|
137
142
|
/** Default `--cache-reuse` chunk when neither config nor manifest set it. */
|
|
138
143
|
export declare const DEFAULT_CACHE_REUSE = 256;
|
|
@@ -100,7 +100,7 @@ function normalizeFlagKey(key) {
|
|
|
100
100
|
* `args` array.
|
|
101
101
|
*/
|
|
102
102
|
export function buildLlamaCppEngineArgs(input) {
|
|
103
|
-
const { config, perModel, planner, kvCacheType, slots, ggufHasMtp, installedDraftModelPath, reasoningFormat, reasoningPreserve, backend, hostCpuCount, } = input;
|
|
103
|
+
const { config, perModel, planner, kvCacheType, slots, ggufHasMtp, installedDraftModelPath, reasoningFormat, reasoningPreserve, backend, hostCpuCount, containerCpuLimit, } = input;
|
|
104
104
|
const args = [];
|
|
105
105
|
// ── GPU-layer offload (`--n-gpu-layers`) ──────────────────────────
|
|
106
106
|
const nGpuLayers = config.llamaCppNGpuLayers ?? perModel?.nGpuLayers ?? planner?.nGpuLayers;
|
|
@@ -203,9 +203,12 @@ export function buildLlamaCppEngineArgs(input) {
|
|
|
203
203
|
!(typeof nCpuMoe === 'number' && nCpuMoe > 0) &&
|
|
204
204
|
!(typeof nCpuFfn === 'number' && nCpuFfn > 0) &&
|
|
205
205
|
!extraArgsOwnThreads(config.llamaCppExtraArgs);
|
|
206
|
+
// In a container that allows fewer CPUs than the host has, the all-cores
|
|
207
|
+
// default oversubscribes the quota and spends each period throttled.
|
|
206
208
|
const threads = config.llamaCppThreads ??
|
|
207
209
|
perModel?.threads ??
|
|
208
|
-
(gpuResident ? gpuResidentThreads(hostCpuCount) : undefined)
|
|
210
|
+
(gpuResident ? gpuResidentThreads(hostCpuCount) : undefined) ??
|
|
211
|
+
(extraArgsOwnThreads(config.llamaCppExtraArgs) ? undefined : containerCpuLimit);
|
|
209
212
|
if (typeof threads === 'number')
|
|
210
213
|
args.push('--threads', String(threads));
|
|
211
214
|
const batchSize = config.llamaCppBatchSize ?? perModel?.batchSize;
|
|
@@ -7,5 +7,13 @@
|
|
|
7
7
|
* recipients.
|
|
8
8
|
*/
|
|
9
9
|
export declare function isSuccessfulAsyncHandoff(toolName: string, output: string): boolean;
|
|
10
|
-
|
|
10
|
+
/** The gezel a successful handoff result names, when it names one. */
|
|
11
|
+
export declare function asyncHandoffRecipient(output: string): string | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* The reply the person sees when the runtime ends a delegating turn, which the
|
|
14
|
+
* model also reads back as its own last message. The tool result already told
|
|
15
|
+
* the model to end its turn, so this line only says what happened. Each entry
|
|
16
|
+
* is one handoff's recipient, or undefined when its result named nobody.
|
|
17
|
+
*/
|
|
18
|
+
export declare function asyncHandoffClosing(recipients: readonly (string | undefined)[]): string;
|
|
11
19
|
export declare function immediateFileWriteClosing(paths: string[]): string;
|
|
@@ -10,10 +10,38 @@ export function isSuccessfulAsyncHandoff(toolName, output) {
|
|
|
10
10
|
return (!output.startsWith('ERROR:') &&
|
|
11
11
|
(toolName === 'message_gezel' || toolName.startsWith('delegate_')));
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
/**
|
|
14
|
+
* The handoff results (core's `messageGezelText`, the MCP `delegate_*` tools)
|
|
15
|
+
* name the recipient after a fixed lead-in and again as "<name>'s turn".
|
|
16
|
+
* Requiring both means a reworded result, or a name holding a period, gives
|
|
17
|
+
* no name rather than half of one.
|
|
18
|
+
*/
|
|
19
|
+
const HANDOFF_RECIPIENT = /(?:Accepted the (?:message|handoff) for|Dispatched the (?:message|handoff) to|An identical file handoff is already pending with) (.+?)(?= \(|[.;])[\s\S]*?\1's turn/;
|
|
20
|
+
/** The gezel a successful handoff result names, when it names one. */
|
|
21
|
+
export function asyncHandoffRecipient(output) {
|
|
22
|
+
return HANDOFF_RECIPIENT.exec(output.slice(0, 2_000))?.[1]?.trim() || undefined;
|
|
23
|
+
}
|
|
24
|
+
function joinNames(names) {
|
|
25
|
+
if (names.length <= 2)
|
|
26
|
+
return names.join(' and ');
|
|
27
|
+
return `${names.slice(0, -1).join(', ')}, and ${names[names.length - 1]}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The reply the person sees when the runtime ends a delegating turn, which the
|
|
31
|
+
* model also reads back as its own last message. The tool result already told
|
|
32
|
+
* the model to end its turn, so this line only says what happened. Each entry
|
|
33
|
+
* is one handoff's recipient, or undefined when its result named nobody.
|
|
34
|
+
*/
|
|
35
|
+
export function asyncHandoffClosing(recipients) {
|
|
36
|
+
const named = [...new Set(recipients.filter((name) => Boolean(name)))];
|
|
37
|
+
if (named.length > 0 && !recipients.includes(undefined)) {
|
|
38
|
+
return named.length === 1
|
|
39
|
+
? `${named[0]} is on it. Their reply will appear here.`
|
|
40
|
+
: `${joinNames(named)} are on it. Their replies will appear here.`;
|
|
41
|
+
}
|
|
42
|
+
return recipients.length > 1
|
|
43
|
+
? "I've passed this on. The replies will appear here."
|
|
44
|
+
: "I've passed this on. The reply will appear here.";
|
|
17
45
|
}
|
|
18
46
|
export function immediateFileWriteClosing(paths) {
|
|
19
47
|
const unique = [...new Set(paths.filter((path) => path.trim().length > 0))];
|
|
@@ -51,7 +51,7 @@ import { coerceToolCallArgs } from '../tool-arg-schema-coercion.js';
|
|
|
51
51
|
import { createToolExecutor } from '../tool-executor.js';
|
|
52
52
|
import { ToolFailureTracker } from '../tool-failure-tracker.js';
|
|
53
53
|
import { ToolRepeatTracker } from '../tool-repeat-tracker.js';
|
|
54
|
-
import { asyncHandoffClosing, immediateFileWriteClosing, isSuccessfulAsyncHandoff, } from './async-file-handoff.js';
|
|
54
|
+
import { asyncHandoffClosing, asyncHandoffRecipient, immediateFileWriteClosing, isSuccessfulAsyncHandoff, } from './async-file-handoff.js';
|
|
55
55
|
export { isSuccessfulAsyncHandoff };
|
|
56
56
|
import { setChatTemplateKwarg, } from './chat-protocol.js';
|
|
57
57
|
import { MlxEngineGate } from './engine-gate.js';
|
|
@@ -60,7 +60,7 @@ import { EngineLogRouter } from './engine-log-router.js';
|
|
|
60
60
|
import { MlxEngineStatsSampler } from './engine-stats-sampler.js';
|
|
61
61
|
import { REASONING_OPEN_HEADER, StreamingReasoningSplit, engineOpensReasoning, reopenTemplateReasoning, requestOpensReasoning, } from './reasoning-stream.js';
|
|
62
62
|
import { applyMlxRequestShape } from './request-shape.js';
|
|
63
|
-
import { PRE_FIRST_BYTE_BASE_MS, buildMidStreamDropMessage, buildPreFirstByteAbortMessage, computePreFirstByteBudgetMs, estimatePromptTokens, formatTps, isMidStreamConnectionDrop, mlxHttpError, } from './runtime-diagnostics.js';
|
|
63
|
+
import { MLX_IMAGE_RESERVED_CHARS, PRE_FIRST_BYTE_BASE_MS, buildMidStreamDropMessage, buildPreFirstByteAbortMessage, computePreFirstByteBudgetMs, estimatePromptTokens, formatTps, isMidStreamConnectionDrop, mlxHttpError, } from './runtime-diagnostics.js';
|
|
64
64
|
import { classifyMlxFatalErrorLine, classifyMlxStartupLine, } from './stdout-parser.js';
|
|
65
65
|
import { salvageTextualToolCalls } from './textual-tool-call-salvage.js';
|
|
66
66
|
import { APPEND_TO_FILE_CONTINUATION_TOOL, MlxToolCallAccumulator, applyCallableToolGrammar, callableRequestToolNames, chatCompletionToolName, externalToolsAsChatCompletions, hermesRequiredArgGrammarRequested, missingTopLevelRequiredToolArgs, toChatCompletionsTools, validatorReportedMissingRequiredArgs, } from './tool-call-protocol.js';
|
|
@@ -73,7 +73,7 @@ export { buildMidStreamDropMessage, computePreFirstByteBudgetMs, estimatePromptT
|
|
|
73
73
|
import { FILE_REPAIR_MUTATION_TOOLS, FILE_REPAIR_READ_TOOLS, applyConstrainedTurnShape, disableTemplateThinking, fileRepairTargetPath, isSourceFileRepairPrompt, requiredToolOnly, writeFileOnlyTools, } from '../constrained-turn.js';
|
|
74
74
|
import { hasExplicitFullFileRewriteWording } from '../direct-file-work-prompt.js';
|
|
75
75
|
import { buildImmediateFileWriteNoMutationNudge, buildPrerequisiteRepairReadNudge, buildScenarioRepairNoMutationNudge, completeWorkspaceReadPaths, extractPrerequisiteRepairReadPaths, normalizeWorkspacePathForCompare, remainingPrerequisiteRepairReadPaths, } from '../file-repair-policy.js';
|
|
76
|
-
import { LOCAL_TURN_LIMITS, LocalTurnPolicy, compactLocalTurn, planFileTurn, repairTools, turnCheckpoint, } from '../local-turn-policy.js';
|
|
76
|
+
import { ConstrainedWriteRecovery, LOCAL_TURN_LIMITS, LocalTurnPolicy, compactLocalTurn, planFileTurn, repairTools, turnCheckpoint, } from '../local-turn-policy.js';
|
|
77
77
|
const MAX_TOOL_LOOP_TURNS = LOCAL_TURN_LIMITS.iterations;
|
|
78
78
|
// Hard per-turn ceiling when the caller doesn't pass `opts.timeoutMs`.
|
|
79
79
|
// 10 minutes matches what the manager passes for local providers in
|
|
@@ -646,7 +646,7 @@ class MlxSession extends StreamingSessionBase {
|
|
|
646
646
|
total += m.content.length;
|
|
647
647
|
// The sidecar bounds images to 1024px. Reserve patch tokens without
|
|
648
648
|
// counting base64 bytes as language tokens or treating pixels as free.
|
|
649
|
-
total += (m.images?.length ?? 0) *
|
|
649
|
+
total += (m.images?.length ?? 0) * MLX_IMAGE_RESERVED_CHARS;
|
|
650
650
|
if (m.tool_calls) {
|
|
651
651
|
for (const tc of m.tool_calls)
|
|
652
652
|
total += tc.function.arguments.length + tc.function.name.length;
|
|
@@ -984,6 +984,7 @@ class MlxSession extends StreamingSessionBase {
|
|
|
984
984
|
// validation error every iteration, "diagnosing" the schema in
|
|
985
985
|
// chat, and re-trying the same wrong shape forever. See the
|
|
986
986
|
// tracker's docstring for the threshold rationale.
|
|
987
|
+
const writeRecovery = new ConstrainedWriteRecovery();
|
|
987
988
|
const failureTracker = new ToolFailureTracker({
|
|
988
989
|
surgicalEditsAvailable,
|
|
989
990
|
delegationAvailable,
|
|
@@ -1164,7 +1165,9 @@ class MlxSession extends StreamingSessionBase {
|
|
|
1164
1165
|
log.debug(`turn#${seq}.${turn} required-call turn: ${opts.requiredTool} only`);
|
|
1165
1166
|
}
|
|
1166
1167
|
}
|
|
1167
|
-
const fileTurnPlan =
|
|
1168
|
+
const fileTurnPlan = writeRecovery.active
|
|
1169
|
+
? { kind: 'ordinary' }
|
|
1170
|
+
: planFileTurn(prompt, tools, opts?.fileTurnIntent);
|
|
1168
1171
|
const unreadInputs = unreadRequiredInputs(requiredInputs, requiredInputReads);
|
|
1169
1172
|
const immediateFileWriteTurn = fileTurnPlan.kind === 'create-file' && unreadInputs.length === 0;
|
|
1170
1173
|
if (fileTurnPlan.kind === 'create-file' &&
|
|
@@ -1219,6 +1222,11 @@ class MlxSession extends StreamingSessionBase {
|
|
|
1219
1222
|
(typeof body.max_tokens !== 'number' || body.max_tokens < FILE_WRITE_MIN_TOKENS)) {
|
|
1220
1223
|
body.max_tokens = FILE_WRITE_MIN_TOKENS;
|
|
1221
1224
|
}
|
|
1225
|
+
if (writeRecovery.lookupPending) {
|
|
1226
|
+
requestTools = tools?.filter((tool) => chatCompletionToolName(tool) === writeRecovery.lookupPending);
|
|
1227
|
+
applyConstrainedTurnShape(body);
|
|
1228
|
+
log.debug(`turn#${seq}.${turn} grounding recovery: research-only surface=${writeRecovery.lookupPending}`);
|
|
1229
|
+
}
|
|
1222
1230
|
if (requestTools && requestTools.length > 0)
|
|
1223
1231
|
body.tools = requestTools;
|
|
1224
1232
|
// Tool-schema size breakdown (opt-in: GEZEL_PROMPT_BREAKDOWN=1). The
|
|
@@ -1419,7 +1427,8 @@ class MlxSession extends StreamingSessionBase {
|
|
|
1419
1427
|
// - STREAMING_IDLE_MS: budget between two consecutive SSE
|
|
1420
1428
|
// content events once tokens are flowing. Generous enough
|
|
1421
1429
|
// to cover a heavy reasoning gap on a 26B model.
|
|
1422
|
-
|
|
1430
|
+
// The request as sent: an image retired from it costs no prefill.
|
|
1431
|
+
const approxPromptTokens = estimatePromptTokens(body.messages, body.tools);
|
|
1423
1432
|
const PRE_FIRST_BYTE_IDLE_MS = computePreFirstByteBudgetMs(approxPromptTokens);
|
|
1424
1433
|
const PRE_FIRST_BYTE_PER_PROGRESS_MS = PRE_FIRST_BYTE_IDLE_MS;
|
|
1425
1434
|
const STREAMING_IDLE_MS = 300_000;
|
|
@@ -2160,6 +2169,11 @@ class MlxSession extends StreamingSessionBase {
|
|
|
2160
2169
|
});
|
|
2161
2170
|
turnContent = salvaged.turnContent;
|
|
2162
2171
|
const { proseRepaired, xmlTagRepaired, claudeInvokeRepaired, glmRepaired, hermesRepaired, shellRepaired, envelopeRepaired, envelopeTruncated, unknownCall, truncatedSalvage, bareInvokeRepaired, } = salvaged;
|
|
2172
|
+
// A prose-shaped call such as lookup_record({}) was ordinary TEXT
|
|
2173
|
+
// to the Hermes grammar, not a constrained tool envelope. Missing
|
|
2174
|
+
// arguments in that salvage cannot establish a decoding violation.
|
|
2175
|
+
// Keep provenance by id through coercion and duplicate collapse.
|
|
2176
|
+
const proseCallIds = new Set(proseRepaired.map((call) => call.id));
|
|
2163
2177
|
const rawTurnContentBeforeReasoning = turnContent;
|
|
2164
2178
|
// Reasoning-tag extraction applies regardless of which (if any)
|
|
2165
2179
|
// salvage path fired. Qwen 3.5/3.6 leak `<think>…</think>`
|
|
@@ -2594,7 +2608,7 @@ class MlxSession extends StreamingSessionBase {
|
|
|
2594
2608
|
tool_calls: toolCalls,
|
|
2595
2609
|
});
|
|
2596
2610
|
let abortDueToFailureLoop = null;
|
|
2597
|
-
|
|
2611
|
+
const asyncHandoffRecipients = [];
|
|
2598
2612
|
let terminalActionClosing = null;
|
|
2599
2613
|
const toolImages = [];
|
|
2600
2614
|
const immediateFileWritePaths = [];
|
|
@@ -2693,16 +2707,21 @@ class MlxSession extends StreamingSessionBase {
|
|
|
2693
2707
|
this.deps.bridges.hasTool(call.function.name)) {
|
|
2694
2708
|
const missingRequired = missingTopLevelRequiredToolArgs(toolArgSchemas.get(call.function.name), args);
|
|
2695
2709
|
const grammarContractViolation = missingRequired.length > 0 &&
|
|
2710
|
+
!proseCallIds.has(call.id) &&
|
|
2696
2711
|
hermesRequiredArgGrammarRequested(body) &&
|
|
2697
2712
|
!this.forceSequentialToolGrammar;
|
|
2698
|
-
if (
|
|
2699
|
-
|
|
2700
|
-
|
|
2713
|
+
if (missingRequired.length > 0) {
|
|
2714
|
+
if (grammarContractViolation) {
|
|
2715
|
+
this.forceSequentialToolGrammar = true;
|
|
2716
|
+
log.warn(`turn#${seq}.${turn} required-argument grammar contract violation tool=${call.function.name} missing=${missingRequired.join(',')} — latching this session to sequential decode`);
|
|
2717
|
+
}
|
|
2701
2718
|
// Do not send a call that the advertised schema proves cannot
|
|
2702
|
-
// pass validation
|
|
2703
|
-
//
|
|
2704
|
-
//
|
|
2705
|
-
output =
|
|
2719
|
+
// pass validation, regardless of how it was parsed. A rejected
|
|
2720
|
+
// prose salvage gets ordinary corrective feedback and retains
|
|
2721
|
+
// speculation; native grammar violations keep the circuit breaker.
|
|
2722
|
+
output = grammarContractViolation
|
|
2723
|
+
? `ERROR: ${call.function.name} was not executed because the constrained decoder omitted required fields: ${missingRequired.join(', ')}. The runtime switched this session to sequential decoding; retry the call once.`
|
|
2724
|
+
: `ERROR: ${call.function.name} was not executed because its arguments are missing required fields: ${missingRequired.join(', ')}. Retry the call with all declared required fields.`;
|
|
2706
2725
|
}
|
|
2707
2726
|
else {
|
|
2708
2727
|
try {
|
|
@@ -2733,6 +2752,7 @@ class MlxSession extends StreamingSessionBase {
|
|
|
2733
2752
|
// the advertised schema. Sequential decode may still recover on
|
|
2734
2753
|
// the next iteration, and the log now makes the skew explicit.
|
|
2735
2754
|
if (validatorReportedMissingRequiredArgs(output) &&
|
|
2755
|
+
!proseCallIds.has(call.id) &&
|
|
2736
2756
|
hermesRequiredArgGrammarRequested(body) &&
|
|
2737
2757
|
!this.forceSequentialToolGrammar) {
|
|
2738
2758
|
this.forceSequentialToolGrammar = true;
|
|
@@ -2747,7 +2767,7 @@ class MlxSession extends StreamingSessionBase {
|
|
|
2747
2767
|
askedQuestionThisTurn = true;
|
|
2748
2768
|
}
|
|
2749
2769
|
if (isSuccessfulAsyncHandoff(call.function.name, output)) {
|
|
2750
|
-
|
|
2770
|
+
asyncHandoffRecipients.push(asyncHandoffRecipient(output));
|
|
2751
2771
|
}
|
|
2752
2772
|
if ((immediateFileWriteTurn || writeContinuationActive) &&
|
|
2753
2773
|
(call.function.name === 'write_file' || call.function.name === 'append_to_file') &&
|
|
@@ -2803,6 +2823,7 @@ class MlxSession extends StreamingSessionBase {
|
|
|
2803
2823
|
}
|
|
2804
2824
|
}
|
|
2805
2825
|
const tracked = failureTracker.recordResult(call.function.name, output);
|
|
2826
|
+
writeRecovery.recordResult(call.function.name, output, tracked, tools);
|
|
2806
2827
|
terminalActionClosing ??= terminalToolClosingText(this.deps.terminalToolPolicy, call.function.name, args, output);
|
|
2807
2828
|
// Layer the same-args repeat tracker on top of the failure
|
|
2808
2829
|
// tracker. The failure tracker catches "tool keeps erroring";
|
|
@@ -2877,7 +2898,7 @@ class MlxSession extends StreamingSessionBase {
|
|
|
2877
2898
|
});
|
|
2878
2899
|
}
|
|
2879
2900
|
this.turnPolicy.checkpoint();
|
|
2880
|
-
if (fileRepairTurn && repairMutationSucceeded) {
|
|
2901
|
+
if (fileRepairTurn && repairMutationSucceeded && !writeRecovery.active) {
|
|
2881
2902
|
const closing = 'Updated the requested file.';
|
|
2882
2903
|
this.messages.push({ role: 'assistant', content: closing });
|
|
2883
2904
|
return closing;
|
|
@@ -2898,7 +2919,7 @@ class MlxSession extends StreamingSessionBase {
|
|
|
2898
2919
|
log.info(`turn#${seq} END terminal-tool-bail afterMs=${Date.now() - start} loopTurns=${turn + 1}`);
|
|
2899
2920
|
return fullText;
|
|
2900
2921
|
}
|
|
2901
|
-
if (immediateFileWritePaths.length > 0) {
|
|
2922
|
+
if (immediateFileWritePaths.length > 0 && !writeRecovery.active) {
|
|
2902
2923
|
// File was truncated mid-content and we still have continuation
|
|
2903
2924
|
// budget — keep the turn alive instead of bailing on a partial.
|
|
2904
2925
|
// The tool result already carries the "append the rest" hint;
|
|
@@ -2921,12 +2942,12 @@ class MlxSession extends StreamingSessionBase {
|
|
|
2921
2942
|
`${writeContinuations > 0 ? ` continuations=${writeContinuations}` : ''}`);
|
|
2922
2943
|
return fullText;
|
|
2923
2944
|
}
|
|
2924
|
-
if (
|
|
2925
|
-
const closingText = asyncHandoffClosing(
|
|
2945
|
+
if (asyncHandoffRecipients.length > 0) {
|
|
2946
|
+
const closingText = asyncHandoffClosing(asyncHandoffRecipients);
|
|
2926
2947
|
this.messages.push({ role: 'assistant', content: closingText });
|
|
2927
2948
|
fullText = closingText;
|
|
2928
2949
|
log.info(`turn#${seq} END async-handoff-bail afterMs=${Date.now() - start} ` +
|
|
2929
|
-
`handoffs=${
|
|
2950
|
+
`handoffs=${asyncHandoffRecipients.length} loopTurns=${turn + 1}`);
|
|
2930
2951
|
return fullText;
|
|
2931
2952
|
}
|
|
2932
2953
|
if (forceProjectMacroBail) {
|
|
@@ -2759,10 +2759,17 @@ class BatchEngine:
|
|
|
2759
2759
|
cache_seed.trim_layers(cache_layers, excess)
|
|
2760
2760
|
except Exception: # noqa: BLE001 — snapshot path covers hybrids
|
|
2761
2761
|
pass
|
|
2762
|
+
cache_tokens = full_prompt + [int(t) for t in sub.token_ids]
|
|
2763
|
+
if cache_seed.all_trimmable(cache_layers):
|
|
2764
|
+
cache_tokens = spec_decode.cached_token_prefix(cache_layers, cache_tokens)
|
|
2765
|
+
if cache_tokens is None:
|
|
2766
|
+
# Leave any prior valid entry intact rather than persisting
|
|
2767
|
+
# cache layers whose token boundary cannot be established.
|
|
2768
|
+
cache_layers = None
|
|
2762
2769
|
r = types.SimpleNamespace(
|
|
2763
2770
|
finish_reason=finish_reason,
|
|
2764
|
-
prompt_cache=list(cache_layers),
|
|
2765
|
-
all_tokens=
|
|
2771
|
+
prompt_cache=list(cache_layers) if cache_layers is not None else None,
|
|
2772
|
+
all_tokens=cache_tokens,
|
|
2766
2773
|
token=sub.token_ids[-1] if sub.token_ids else 0,
|
|
2767
2774
|
)
|
|
2768
2775
|
self._finish(sub, r)
|
|
@@ -117,8 +117,11 @@ def resolve_spec(args, model, text_tower: Optional[str], log=print) -> Optional[
|
|
|
117
117
|
except Exception as exc: # noqa: BLE001
|
|
118
118
|
_off(log, f"drafter incompatible with target: {exc}")
|
|
119
119
|
return None
|
|
120
|
-
if
|
|
121
|
-
|
|
120
|
+
if (
|
|
121
|
+
callable(getattr(drafter, "prefill_from_target_hidden", None))
|
|
122
|
+
and not install_chunked_draft_prefill(
|
|
123
|
+
drafter, int(getattr(args, "prefill_step_size", 0) or 0), log=log
|
|
124
|
+
)
|
|
122
125
|
):
|
|
123
126
|
log(
|
|
124
127
|
"[spec] drafter has no incremental prefill surface — long prompts "
|
|
@@ -240,6 +243,11 @@ def draft_prefill_gate(spec: Optional[SpecState], prompt_len: int) -> Tuple[bool
|
|
|
240
243
|
throw that work away."""
|
|
241
244
|
if spec is None:
|
|
242
245
|
return True, ""
|
|
246
|
+
# Shared-KV drafters (e.g. Gemma's assistant) have no separate prompt
|
|
247
|
+
# prefill. Both round loops skip this optional hook, so there is no
|
|
248
|
+
# quadratic drafter prefill to limit for that capability shape.
|
|
249
|
+
if not callable(getattr(spec.drafter, "prefill_from_target_hidden", None)):
|
|
250
|
+
return True, ""
|
|
243
251
|
if getattr(spec.drafter, "_gezel_chunked_prefill", False):
|
|
244
252
|
return True, ""
|
|
245
253
|
if int(prompt_len) > UNCHUNKED_DRAFT_PREFILL_LIMIT:
|
|
@@ -278,9 +286,21 @@ def cache_matches_tower(layers, tower) -> bool:
|
|
|
278
286
|
return False
|
|
279
287
|
if layers is None or len(layers) != len(expect):
|
|
280
288
|
return False
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
289
|
+
|
|
290
|
+
def matches(actual, expected):
|
|
291
|
+
if type(actual).__name__ == type(expected).__name__:
|
|
292
|
+
return True
|
|
293
|
+
# Upstream MTP deliberately wraps a rotating cache in this subclass
|
|
294
|
+
# to roll back rejected drafts. It is still the same target layout;
|
|
295
|
+
# rejecting our own saved cache here forces every follow-up cold.
|
|
296
|
+
# Do not admit arbitrary subclasses (e.g. batch-shaped cache adapters).
|
|
297
|
+
return (
|
|
298
|
+
type(expected).__name__ == "RotatingKVCache"
|
|
299
|
+
and type(actual).__name__ == "BufferedRotatingKVCache"
|
|
300
|
+
and isinstance(actual, type(expected))
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
return all(matches(a, b) for a, b in zip(layers, expect))
|
|
284
304
|
|
|
285
305
|
|
|
286
306
|
def clone_layers(layers) -> List[Any]:
|
|
@@ -294,6 +314,25 @@ def clone_layers(layers) -> List[Any]:
|
|
|
294
314
|
return out
|
|
295
315
|
|
|
296
316
|
|
|
317
|
+
def cached_token_prefix(layers, tokens: Sequence[int]) -> Optional[List[int]]:
|
|
318
|
+
"""Describe only tokens actually fed into a trimmable target cache.
|
|
319
|
+
|
|
320
|
+
A final bonus token can be emitted before it is fed back to the target.
|
|
321
|
+
Saving it as cached makes the next seed fail its offset consistency guard.
|
|
322
|
+
Unknown or disagreeing offsets must remain a miss, never guessed state.
|
|
323
|
+
"""
|
|
324
|
+
try:
|
|
325
|
+
offsets = [int(c.offset) for c in layers]
|
|
326
|
+
except (AttributeError, TypeError, ValueError):
|
|
327
|
+
return None
|
|
328
|
+
if not offsets or len(set(offsets)) != 1:
|
|
329
|
+
return None
|
|
330
|
+
count = offsets[0]
|
|
331
|
+
if count <= 0 or count > len(tokens):
|
|
332
|
+
return None
|
|
333
|
+
return list(tokens[:count])
|
|
334
|
+
|
|
335
|
+
|
|
297
336
|
def chunked_prefill_steps(
|
|
298
337
|
tower,
|
|
299
338
|
cache,
|