@bendyline/gezel-inference 1.0.3 → 1.0.5
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/catalog/chat-models/index.json +1 -1
- package/dist/catalog/chat-models/na/nanbeige4.2-3b-q4/manifest.json +1 -1
- package/dist/catalog/chat-models/ne/nemotron3.5-bolt-e4b-q4/manifest.json +142 -0
- package/dist/catalog/chat-models/ne/nemotron3.5-bolt-e4b-q4/versions/1.0.0/manifest.json +14 -0
- package/dist/catalog/chat-models/ne/nemotron3.5-bolt-e4b-q4/versions/1.0.1/manifest.json +14 -0
- package/dist/catalog/chat-models/raw-index.json +1 -1
- package/dist/catalog/image-models/an/animagine-xl-4/manifest.json +40 -0
- package/dist/catalog/image-models/an/animagine-xl-4/versions/4.0.0/manifest.json +10 -0
- package/dist/catalog/image-models/an/animagine-xl-4/versions/4.0.1/manifest.json +16 -0
- package/dist/catalog/image-models/fl/flux-2-klein-4b-q4/manifest.json +44 -0
- package/dist/catalog/image-models/fl/flux-2-klein-4b-q4/versions/1.0.0/manifest.json +23 -0
- package/dist/catalog/image-models/fl/flux-2-klein-4b-q4/versions/1.0.1/manifest.json +29 -0
- package/dist/catalog/image-models/index.json +1 -0
- package/dist/catalog/image-models/ir/iris-3b-f32/manifest.json +41 -0
- package/dist/catalog/image-models/ir/iris-3b-f32/versions/1.0.0/manifest.json +19 -0
- package/dist/catalog/image-models/ir/iris-3b-f32/versions/1.0.1/manifest.json +25 -0
- package/dist/catalog/image-models/ir/iris-3b-f32/versions/1.0.2/manifest.json +25 -0
- package/dist/catalog/image-models/kr/krea-2-turbo-q4/manifest.json +42 -0
- package/dist/catalog/image-models/kr/krea-2-turbo-q4/versions/1.0.0/manifest.json +23 -0
- package/dist/catalog/image-models/kr/krea-2-turbo-q4/versions/1.0.1/manifest.json +29 -0
- package/dist/catalog/image-models/kr/krea-2-turbo-q6/manifest.json +42 -0
- package/dist/catalog/image-models/kr/krea-2-turbo-q6/versions/1.0.0/manifest.json +23 -0
- package/dist/catalog/image-models/kr/krea-2-turbo-q6/versions/1.0.1/manifest.json +29 -0
- package/dist/catalog/image-models/kr/krea-2-turbo-q8/manifest.json +42 -0
- package/dist/catalog/image-models/kr/krea-2-turbo-q8/versions/1.0.0/manifest.json +23 -0
- package/dist/catalog/image-models/kr/krea-2-turbo-q8/versions/1.0.1/manifest.json +29 -0
- package/dist/catalog/image-models/mi/ming-image-0.1-design-bf16/manifest.json +41 -0
- package/dist/catalog/image-models/mi/ming-image-0.1-design-bf16/versions/1.0.0/manifest.json +31 -0
- package/dist/catalog/image-models/mi/ming-image-0.1-design-bf16/versions/1.0.1/manifest.json +50 -0
- package/dist/catalog/image-models/mi/ming-image-0.1-design-bf16/versions/1.0.2/manifest.json +50 -0
- package/dist/catalog/image-models/mo/moss-image-sft-q4/manifest.json +40 -0
- package/dist/catalog/image-models/mo/moss-image-sft-q4/versions/1.0.0/manifest.json +37 -0
- package/dist/catalog/image-models/mo/moss-image-sft-q4/versions/1.0.1/manifest.json +37 -0
- package/dist/catalog/image-models/raw-index.json +1 -0
- package/dist/catalog/image-models/sd/sd-1.5/manifest.json +40 -0
- package/dist/catalog/image-models/sd/sd-1.5/versions/1.0.0/manifest.json +10 -0
- package/dist/catalog/image-models/sd/sd-1.5/versions/1.0.1/manifest.json +16 -0
- package/dist/catalog/image-models/sd/sd-turbo/manifest.json +40 -0
- package/dist/catalog/image-models/sd/sd-turbo/versions/1.0.0/manifest.json +10 -0
- package/dist/catalog/image-models/sd/sd-turbo/versions/1.0.1/manifest.json +16 -0
- package/dist/catalog/image-models/sd/sdxl-base-1.0/manifest.json +39 -0
- package/dist/catalog/image-models/sd/sdxl-base-1.0/versions/1.0.0/manifest.json +10 -0
- package/dist/catalog/image-models/sd/sdxl-base-1.0/versions/1.0.1/manifest.json +16 -0
- package/dist/catalog/image-models/sd/sdxl-lightning-4step/manifest.json +40 -0
- package/dist/catalog/image-models/sd/sdxl-lightning-4step/versions/1.0.0/manifest.json +10 -0
- package/dist/catalog/image-models/sd/sdxl-lightning-4step/versions/1.0.1/manifest.json +16 -0
- package/dist/catalog/image-models/sd/sdxl-turbo/manifest.json +41 -0
- package/dist/catalog/image-models/sd/sdxl-turbo/versions/1.0.0/manifest.json +10 -0
- package/dist/catalog/image-models/sd/sdxl-turbo/versions/1.0.1/manifest.json +16 -0
- package/dist/catalog/provenance.json +2 -1
- package/dist/engines/native-file-manifest.json +129 -119
- package/dist/engines/native-manifest.d.ts +4 -4
- package/dist/engines/native-manifest.js +36 -36
- package/dist/engines/resolver.js +9 -0
- package/dist/http/openai-compat/engine-failure.js +7 -2
- package/dist/http/openai-compat/responses-streaming.d.ts +13 -0
- package/dist/http/openai-compat/responses-streaming.js +30 -0
- package/dist/http/openai-compat/streaming.d.ts +9 -0
- package/dist/http/openai-compat/streaming.js +49 -9
- package/dist/http/openai-compat/tool-call-screen.d.ts +28 -0
- package/dist/http/openai-compat/tool-call-screen.js +43 -0
- package/dist/http/routes/v1-chat.js +15 -1
- package/dist/http/routes/v1-images.d.ts +44 -0
- package/dist/http/routes/v1-images.js +370 -0
- package/dist/http/routes/v1-responses.js +13 -1
- package/dist/inference-service.d.ts +3 -0
- package/dist/inference-service.js +24 -2
- package/dist/kokoro-lexicon/manifest.json +6 -3
- package/dist/models/image-fit.d.ts +83 -0
- package/dist/models/image-fit.js +163 -0
- package/dist/models/weight-facts.d.ts +36 -0
- package/dist/models/weight-facts.js +171 -0
- package/dist/providers/ds4/build-provider.js +28 -21
- package/dist/providers/ds4/dspark.d.ts +23 -5
- package/dist/providers/ds4/dspark.js +36 -8
- package/dist/providers/image/img2img-support.d.ts +40 -0
- package/dist/providers/image/img2img-support.js +74 -0
- package/dist/providers/image/local-engine.d.ts +31 -0
- package/dist/providers/image/local-engine.js +47 -0
- package/dist/providers/image/local-factory.d.ts +95 -0
- package/dist/providers/image/local-factory.js +270 -0
- package/dist/providers/image/mock.d.ts +23 -0
- package/dist/providers/image/mock.js +151 -0
- package/dist/providers/image/pull-registry.d.ts +106 -0
- package/dist/providers/image/pull-registry.js +227 -0
- package/dist/providers/image/sd-cpp.d.ts +200 -0
- package/dist/providers/image/sd-cpp.js +845 -0
- package/dist/providers/image/types.d.ts +237 -0
- package/dist/providers/image/types.js +20 -0
- package/dist/providers/llama-cpp/backend-health.d.ts +7 -0
- package/dist/providers/llama-cpp/backend-health.js +41 -1
- package/dist/providers/mlx/provider.js +13 -3
- package/dist/providers/native/supervisor.d.ts +8 -0
- package/dist/providers/native/supervisor.js +52 -4
- package/dist/providers/project-macro-loop-bail.js +5 -3
- package/dist/providers/provider-lifecycle.d.ts +21 -0
- package/dist/providers/provider-lifecycle.js +83 -0
- package/dist/providers/retirement-gate.d.ts +20 -0
- package/dist/providers/retirement-gate.js +55 -0
- package/dist/providers/types.d.ts +7 -1
- package/dist/system/memory.js +6 -0
- package/dist/utils/image-meta.d.ts +12 -0
- package/dist/utils/image-meta.js +77 -0
- package/package.json +6 -6
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Can this machine run that image model?" for the image catalog.
|
|
3
|
+
*
|
|
4
|
+
* Prices each catalog entry from the weight facts its gilde entry pins (core
|
|
5
|
+
* `media-memory.ts`) — never the network — and checks it against the same
|
|
6
|
+
* static ceilings the device ledger refuses at, so `wont-run` here means the
|
|
7
|
+
* launch path would refuse the model on this machine whatever else is
|
|
8
|
+
* running. The download pages hide those models and the pull routes refuse
|
|
9
|
+
* them. Contract: docs/media-model-memory.md.
|
|
10
|
+
*
|
|
11
|
+
* The video half lives with the video engine in the service, which shares
|
|
12
|
+
* the helpers here.
|
|
13
|
+
*/
|
|
14
|
+
import { totalmem } from 'node:os';
|
|
15
|
+
import { MEDIA_MODEL_WONT_RUN_CODE, assessMediaFit, createLogger, imageMemoryRequirement, imageModelDefaultSide, resolveImageSize, } from '@bendyline/gezel';
|
|
16
|
+
import { isCudaArchivePath } from '@bendyline/gezel/native';
|
|
17
|
+
import { sdServerLoadProfile } from '../providers/image/sd-cpp.js';
|
|
18
|
+
import { configuredEngineMemoryBytes, staticCapacityCeilings, } from '../providers/native/device-capacity.js';
|
|
19
|
+
import { measuredCapacityBudget } from '../providers/native/measured-budget.js';
|
|
20
|
+
import { detectMemoryProfileCached } from '../system/memory.js';
|
|
21
|
+
import { imageCatalogWeightFiles, sdCppMainComponent } from './weight-facts.js';
|
|
22
|
+
const log = createLogger('media-fit');
|
|
23
|
+
/**
|
|
24
|
+
* The 409 body a pull route answers with when the model cannot run here, or
|
|
25
|
+
* null to let the pull start. A failure to assess never blocks a download:
|
|
26
|
+
* the launch path still applies the same check.
|
|
27
|
+
*/
|
|
28
|
+
export async function wontRunRefusal(assess) {
|
|
29
|
+
try {
|
|
30
|
+
const fit = await assess();
|
|
31
|
+
if (fit?.verdict !== 'wont-run')
|
|
32
|
+
return null;
|
|
33
|
+
return {
|
|
34
|
+
error: `This model is too large for this machine. ${fit.reason}`,
|
|
35
|
+
code: MEDIA_MODEL_WONT_RUN_CODE,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
log.warn(`[media-fit] could not assess a pull, letting it start: ${error instanceof Error ? error.message : String(error)}`);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Where sd-server keeps weights: Metal on Apple Silicon, CPU on Intel Macs,
|
|
45
|
+
* CUDA or Vulkan elsewhere, falling back to the processor when no GPU
|
|
46
|
+
* answers. Windows on Arm runs on the processor unless discovery picked its
|
|
47
|
+
* CUDA build, which only an RTX Spark gets.
|
|
48
|
+
*/
|
|
49
|
+
export function imageMediaPool(kind, host = {
|
|
50
|
+
platform: process.platform,
|
|
51
|
+
arch: process.arch,
|
|
52
|
+
sdServerBin: process.env.GEZEL_SD_SERVER_BIN,
|
|
53
|
+
}) {
|
|
54
|
+
if (host.platform === 'darwin')
|
|
55
|
+
return host.arch === 'arm64' ? 'unified' : 'cpu';
|
|
56
|
+
if (host.platform === 'win32' &&
|
|
57
|
+
host.arch === 'arm64' &&
|
|
58
|
+
!(host.sdServerBin && isCudaArchivePath(host.sdServerBin))) {
|
|
59
|
+
return 'cpu';
|
|
60
|
+
}
|
|
61
|
+
return kind === 'discrete-gpu' ? 'discrete' : kind === 'unified' ? 'unified' : 'cpu';
|
|
62
|
+
}
|
|
63
|
+
/** The ceilings the device ledger applies on this machine, automatic and under the person's limit. */
|
|
64
|
+
export async function mediaCapacity(pool, readConfig) {
|
|
65
|
+
const budget = await measuredCapacityBudget();
|
|
66
|
+
const ram = totalmem();
|
|
67
|
+
const auto = staticCapacityCeilings(budget, ram);
|
|
68
|
+
const configured = configuredEngineMemoryBytes(await readConfig().catch(() => ({})));
|
|
69
|
+
const limited = configured === undefined ? undefined : staticCapacityCeilings(budget, ram, configured);
|
|
70
|
+
const profile = pool === 'discrete' ? await detectMemoryProfileCached().catch(() => null) : null;
|
|
71
|
+
return {
|
|
72
|
+
pool,
|
|
73
|
+
...auto,
|
|
74
|
+
...(limited && limited.budgetBytes < auto.budgetBytes ? { limit: limited } : {}),
|
|
75
|
+
totalRamBytes: ram,
|
|
76
|
+
...(profile?.gpuLargestDeviceBytes ? { gpuDeviceBytes: profile.gpuLargestDeviceBytes } : {}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** The memory sd-server needs for one model, from its weight files and its launch profile. */
|
|
80
|
+
export function sdServerRequirement(input) {
|
|
81
|
+
const profile = sdServerLoadProfile(input.modelId);
|
|
82
|
+
return imageMemoryRequirement({
|
|
83
|
+
files: input.files,
|
|
84
|
+
pool: input.pool,
|
|
85
|
+
vaeDecode: profile.vaeDecode,
|
|
86
|
+
// The rule names the diffusion weights, which are the main file.
|
|
87
|
+
bf16Components: profile.bf16TensorPattern ? [sdCppMainComponent(input.weightsKind)] : [],
|
|
88
|
+
megapixels: input.megapixels ?? profile.defaultSidePx ** 2 / 1024 ** 2,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* What a launched sd-server claims on the device ledger.
|
|
93
|
+
*
|
|
94
|
+
* On a discrete card the fit figures charge graphics memory for weights only,
|
|
95
|
+
* because sd-server can stream components from RAM, and the download page
|
|
96
|
+
* must not call a model that offloads "won't run". A running render holds its
|
|
97
|
+
* compute buffers on the card too, though: FLUX.2 Klein at 1024px held
|
|
98
|
+
* 9.4 GiB of a 12 GiB card's dedicated memory against 4.9 GiB of weights
|
|
99
|
+
* (RTX 5070 Ti Laptop, Vulkan, M0 spike 2026-10-25). Claiming weights only
|
|
100
|
+
* let a resident chat model keep its memory, and the render then failed at
|
|
101
|
+
* its first step. The claim is the estimated peak, capped at the card so a
|
|
102
|
+
* model that must offload is admitted once the card is otherwise free.
|
|
103
|
+
*/
|
|
104
|
+
export function sdServerLedgerClaim(requirement, cardBytes) {
|
|
105
|
+
if (requirement.pool !== 'discrete') {
|
|
106
|
+
return { bytes: requirement.bytes, gpuBytes: requirement.gpuBytes };
|
|
107
|
+
}
|
|
108
|
+
const gpuBytes = Math.min(Math.max(requirement.gpuPeakBytes, requirement.gpuBytes), cardBytes);
|
|
109
|
+
return { bytes: Math.max(requirement.bytes, gpuBytes), gpuBytes };
|
|
110
|
+
}
|
|
111
|
+
/** Megapixels a model renders at a quality tier, inside its range. */
|
|
112
|
+
export function imageRenderMegapixels(manifest, quality) {
|
|
113
|
+
const { width, height } = resolveImageSize({ quality }, {
|
|
114
|
+
defaultSidePx: imageModelDefaultSide(manifest.id),
|
|
115
|
+
widthRange: manifest.widthRange,
|
|
116
|
+
heightRange: manifest.heightRange,
|
|
117
|
+
});
|
|
118
|
+
return (width * height) / 1024 ** 2;
|
|
119
|
+
}
|
|
120
|
+
async function imagePlan(ctx) {
|
|
121
|
+
const budget = await measuredCapacityBudget();
|
|
122
|
+
const pool = imageMediaPool(budget.kind);
|
|
123
|
+
return { pool, capacity: await mediaCapacity(pool, ctx.readConfig) };
|
|
124
|
+
}
|
|
125
|
+
function assessImageManifest(plan, manifest, quality) {
|
|
126
|
+
return assessMediaFit(sdServerRequirement({
|
|
127
|
+
modelId: manifest.id,
|
|
128
|
+
weightsKind: manifest.weightsKind,
|
|
129
|
+
files: imageCatalogWeightFiles(manifest),
|
|
130
|
+
pool: plan.pool,
|
|
131
|
+
...(quality ? { megapixels: imageRenderMegapixels(manifest, quality) } : {}),
|
|
132
|
+
}), plan.capacity);
|
|
133
|
+
}
|
|
134
|
+
export function mediaFitResponse(plan, models) {
|
|
135
|
+
return {
|
|
136
|
+
pool: plan.pool,
|
|
137
|
+
budgetBytes: plan.capacity.budgetBytes,
|
|
138
|
+
gpuBudgetBytes: plan.capacity.gpuBudgetBytes,
|
|
139
|
+
totalRamBytes: plan.capacity.totalRamBytes,
|
|
140
|
+
models,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Every catalog image model's fit. With `quality`, each is priced at the size
|
|
145
|
+
* it renders at that tier rather than its default, which is how the direct
|
|
146
|
+
* image gezel's settings know whether High will fit.
|
|
147
|
+
*/
|
|
148
|
+
export async function imageCatalogFits(ctx, quality) {
|
|
149
|
+
const plan = await imagePlan(ctx);
|
|
150
|
+
const items = await ctx.catalog.list('image-model');
|
|
151
|
+
const models = items
|
|
152
|
+
.map((item) => item.manifest)
|
|
153
|
+
.filter((m) => m.kind === 'image-model')
|
|
154
|
+
.map((m) => ({ id: m.id, ...assessImageManifest(plan, m, quality) }));
|
|
155
|
+
return mediaFitResponse(plan, models);
|
|
156
|
+
}
|
|
157
|
+
/** One catalog image model's fit, or null when the id is not in the catalog. */
|
|
158
|
+
export async function imageModelFit(ctx, id) {
|
|
159
|
+
const item = await ctx.catalog.get('image-model', id);
|
|
160
|
+
if (!item || item.manifest.kind !== 'image-model')
|
|
161
|
+
return null;
|
|
162
|
+
return assessImageManifest(await imagePlan(ctx), item.manifest);
|
|
163
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weight facts for media-model memory pricing: each weight file's size and
|
|
3
|
+
* how many elements it stores at every precision. Core `media-memory.ts`
|
|
4
|
+
* turns them into a requirement.
|
|
5
|
+
*
|
|
6
|
+
* Two sources that must agree. A catalog entry carries the facts as
|
|
7
|
+
* `tensors` beside each file's sha256, written by gilde's `pin-weight-tensors`
|
|
8
|
+
* tool when the model was authored; the launch path reads the same facts
|
|
9
|
+
* from the header of the file on disk. Files are pinned by sha256, so both
|
|
10
|
+
* describe the same bytes. Nothing here touches the network: an entry
|
|
11
|
+
* without the facts is priced by its plausible range instead.
|
|
12
|
+
*/
|
|
13
|
+
import { type ImageModelManifest, type MediaWeightFile, type VideoModelManifest, type WeightTensors } from '@bendyline/gezel';
|
|
14
|
+
/** Length of the JSON header from a safetensors file's first 8 bytes, or null when implausible. */
|
|
15
|
+
export declare function safetensorsHeaderLength(prefix: Uint8Array, fileBytes: number): number | null;
|
|
16
|
+
/** The `tensors` facts of a safetensors file on disk; undefined when it cannot be read as one. */
|
|
17
|
+
export declare function readLocalWeightTensors(path: string): Promise<WeightTensors | undefined>;
|
|
18
|
+
export declare function videoCatalogWeightFiles(manifest: VideoModelManifest): MediaWeightFile[];
|
|
19
|
+
/**
|
|
20
|
+
* The weight files of an installed diffusers tree, headers read from disk.
|
|
21
|
+
* diffusers loads only the shards a directory's index names, so an unsharded
|
|
22
|
+
* duplicate beside them is never resident and is left out.
|
|
23
|
+
*/
|
|
24
|
+
export declare function localDiffusersWeightFiles(modelDir: string): Promise<MediaWeightFile[]>;
|
|
25
|
+
/** The sd-cpp component the main weights load as: the flag they ride on. */
|
|
26
|
+
export declare function sdCppMainComponent(weightsKind: 'checkpoint' | 'diffusion-model'): string;
|
|
27
|
+
export declare function imageCatalogWeightFiles(manifest: ImageModelManifest): MediaWeightFile[];
|
|
28
|
+
/** The weight files of an installed sd-cpp model, headers read from disk. */
|
|
29
|
+
export declare function localSdCppWeightFiles(model: {
|
|
30
|
+
weightsPath: string;
|
|
31
|
+
weightsKind: 'checkpoint' | 'diffusion-model';
|
|
32
|
+
auxiliaryFiles: ReadonlyArray<{
|
|
33
|
+
role: string;
|
|
34
|
+
path: string;
|
|
35
|
+
}>;
|
|
36
|
+
}): Promise<MediaWeightFile[]>;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weight facts for media-model memory pricing: each weight file's size and
|
|
3
|
+
* how many elements it stores at every precision. Core `media-memory.ts`
|
|
4
|
+
* turns them into a requirement.
|
|
5
|
+
*
|
|
6
|
+
* Two sources that must agree. A catalog entry carries the facts as
|
|
7
|
+
* `tensors` beside each file's sha256, written by gilde's `pin-weight-tensors`
|
|
8
|
+
* tool when the model was authored; the launch path reads the same facts
|
|
9
|
+
* from the header of the file on disk. Files are pinned by sha256, so both
|
|
10
|
+
* describe the same bytes. Nothing here touches the network: an entry
|
|
11
|
+
* without the facts is priced by its plausible range instead.
|
|
12
|
+
*/
|
|
13
|
+
import { open, readFile, readdir, stat } from 'node:fs/promises';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { tallySafetensorsHeader, weightFileFormat, } from '@bendyline/gezel';
|
|
16
|
+
/** A header larger than this is not a safetensors header. */
|
|
17
|
+
const MAX_HEADER_BYTES = 100 * 1024 * 1024;
|
|
18
|
+
/** Length of the JSON header from a safetensors file's first 8 bytes, or null when implausible. */
|
|
19
|
+
export function safetensorsHeaderLength(prefix, fileBytes) {
|
|
20
|
+
if (prefix.byteLength < 8)
|
|
21
|
+
return null;
|
|
22
|
+
const view = new DataView(prefix.buffer, prefix.byteOffset, 8);
|
|
23
|
+
const length = Number(view.getBigUint64(0, true));
|
|
24
|
+
if (!Number.isSafeInteger(length) || length <= 0 || length > MAX_HEADER_BYTES)
|
|
25
|
+
return null;
|
|
26
|
+
if (length + 8 > fileBytes)
|
|
27
|
+
return null;
|
|
28
|
+
return length;
|
|
29
|
+
}
|
|
30
|
+
/** The `tensors` facts of a safetensors file on disk; undefined when it cannot be read as one. */
|
|
31
|
+
export async function readLocalWeightTensors(path) {
|
|
32
|
+
let handle;
|
|
33
|
+
try {
|
|
34
|
+
handle = await open(path, 'r');
|
|
35
|
+
const size = (await handle.stat()).size;
|
|
36
|
+
const prefix = Buffer.alloc(8);
|
|
37
|
+
const { bytesRead } = await handle.read(prefix, 0, 8, 0);
|
|
38
|
+
const length = safetensorsHeaderLength(prefix.subarray(0, bytesRead), size);
|
|
39
|
+
if (length === null)
|
|
40
|
+
return undefined;
|
|
41
|
+
const header = Buffer.alloc(length);
|
|
42
|
+
await handle.read(header, 0, length, 8);
|
|
43
|
+
return tallySafetensorsHeader(JSON.parse(header.toString('utf8')));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await handle?.close();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function catalogFile(component, fileName, sizeBytes, tensors) {
|
|
53
|
+
const format = weightFileFormat(fileName);
|
|
54
|
+
if (!format)
|
|
55
|
+
return null;
|
|
56
|
+
return { component, sizeBytes, format, ...(tensors ? { tensors } : {}) };
|
|
57
|
+
}
|
|
58
|
+
// ─ Video: diffusers trees ───────────────────────────────────────────
|
|
59
|
+
/** The diffusers component a repo-relative file belongs to: its top-level folder. */
|
|
60
|
+
function diffusersComponent(name) {
|
|
61
|
+
const slash = name.indexOf('/');
|
|
62
|
+
return slash < 0 ? '' : name.slice(0, slash);
|
|
63
|
+
}
|
|
64
|
+
export function videoCatalogWeightFiles(manifest) {
|
|
65
|
+
return manifest.source.files.flatMap((file) => {
|
|
66
|
+
const priced = catalogFile(diffusersComponent(file.name), file.name, file.sizeBytes, file.tensors);
|
|
67
|
+
return priced ? [priced] : [];
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The weight files of an installed diffusers tree, headers read from disk.
|
|
72
|
+
* diffusers loads only the shards a directory's index names, so an unsharded
|
|
73
|
+
* duplicate beside them is never resident and is left out.
|
|
74
|
+
*/
|
|
75
|
+
export async function localDiffusersWeightFiles(modelDir) {
|
|
76
|
+
const out = [];
|
|
77
|
+
const walk = async (dir, component, depth) => {
|
|
78
|
+
if (depth > 6)
|
|
79
|
+
return;
|
|
80
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
81
|
+
const indexed = await indexedShards(dir, entries.map((e) => e.name));
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const path = join(dir, entry.name);
|
|
84
|
+
if (entry.isDirectory()) {
|
|
85
|
+
await walk(path, component || entry.name, depth + 1);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!entry.isFile())
|
|
89
|
+
continue;
|
|
90
|
+
const format = weightFileFormat(entry.name);
|
|
91
|
+
if (!format)
|
|
92
|
+
continue;
|
|
93
|
+
if (format === 'safetensors' && indexed && !indexed.has(entry.name))
|
|
94
|
+
continue;
|
|
95
|
+
const sizeBytes = (await stat(path)).size;
|
|
96
|
+
const tensors = format === 'safetensors' ? await readLocalWeightTensors(path) : undefined;
|
|
97
|
+
out.push({ component, sizeBytes, format, ...(tensors ? { tensors } : {}) });
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
await walk(modelDir, '', 0);
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
async function indexedShards(dir, names) {
|
|
104
|
+
const index = names.find((n) => n.endsWith('.safetensors.index.json'));
|
|
105
|
+
if (!index)
|
|
106
|
+
return null;
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(await readFile(join(dir, index), 'utf8'));
|
|
109
|
+
const shards = new Set(Object.values(parsed.weight_map ?? {}));
|
|
110
|
+
return shards.size > 0 ? shards : null;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ─ Image: stable-diffusion.cpp files ────────────────────────────────
|
|
117
|
+
/** The sd-cpp component the main weights load as: the flag they ride on. */
|
|
118
|
+
export function sdCppMainComponent(weightsKind) {
|
|
119
|
+
return weightsKind === 'diffusion-model' ? 'diffusion_model' : 'model';
|
|
120
|
+
}
|
|
121
|
+
function urlFileName(url) {
|
|
122
|
+
try {
|
|
123
|
+
return decodeURIComponent(new URL(url).pathname.split('/').pop() ?? '');
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return '';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export function imageCatalogWeightFiles(manifest) {
|
|
130
|
+
const entries = [
|
|
131
|
+
{
|
|
132
|
+
component: sdCppMainComponent(manifest.weightsKind),
|
|
133
|
+
url: manifest.downloadUrl,
|
|
134
|
+
sizeBytes: manifest.approxSizeBytes,
|
|
135
|
+
tensors: manifest.tensors,
|
|
136
|
+
},
|
|
137
|
+
...manifest.auxiliaryFiles.map((aux) => ({
|
|
138
|
+
component: aux.role,
|
|
139
|
+
url: aux.downloadUrl,
|
|
140
|
+
sizeBytes: aux.approxSizeBytes,
|
|
141
|
+
tensors: aux.tensors,
|
|
142
|
+
})),
|
|
143
|
+
];
|
|
144
|
+
return entries.flatMap((e) => {
|
|
145
|
+
const priced = catalogFile(e.component, urlFileName(e.url), e.sizeBytes, e.tensors);
|
|
146
|
+
return priced ? [priced] : [];
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/** The weight files of an installed sd-cpp model, headers read from disk. */
|
|
150
|
+
export async function localSdCppWeightFiles(model) {
|
|
151
|
+
const entries = [
|
|
152
|
+
{ component: sdCppMainComponent(model.weightsKind), path: model.weightsPath },
|
|
153
|
+
...model.auxiliaryFiles.map((a) => ({ component: a.role, path: a.path })),
|
|
154
|
+
];
|
|
155
|
+
const out = [];
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
const format = weightFileFormat(entry.path);
|
|
158
|
+
if (!format)
|
|
159
|
+
continue;
|
|
160
|
+
let sizeBytes;
|
|
161
|
+
try {
|
|
162
|
+
sizeBytes = (await stat(entry.path)).size;
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const tensors = format === 'safetensors' ? await readLocalWeightTensors(entry.path) : undefined;
|
|
168
|
+
out.push({ component: entry.component, sizeBytes, format, ...(tensors ? { tensors } : {}) });
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
@@ -7,7 +7,7 @@ import { pickFreePort } from '../native/port.js';
|
|
|
7
7
|
import { NativeEngineSupervisor } from '../native/supervisor.js';
|
|
8
8
|
import { patientFetch } from '../patient-fetch.js';
|
|
9
9
|
import { nativeVisionEnabledFor } from '../vision-capability.js';
|
|
10
|
-
import { ds4DsparkArgs, resolveDs4Dspark } from './dspark.js';
|
|
10
|
+
import { DS4_V41_DRAFTER_RUNTIME_BYTES, ds4DsparkArgs, ds4DsparkStreamsWithSsd, resolveDs4Dspark, } from './dspark.js';
|
|
11
11
|
import { ds4MtpArgs, resolveDs4Mtp } from './mtp.js';
|
|
12
12
|
import { Ds4Provider } from './provider.js';
|
|
13
13
|
const log = createLogger('chat');
|
|
@@ -307,36 +307,50 @@ export async function buildDs4Provider(opts) {
|
|
|
307
307
|
if (config.ds4SsdStreaming === true && ds4Source?.ssdStreamingSupported === false) {
|
|
308
308
|
log.warn(`[ds4] ignored SSD-streaming override for ${effectiveModelId ?? modelPath}; this model keeps its lookup tables on disk but does not implement routed-expert streaming`);
|
|
309
309
|
}
|
|
310
|
+
const cachePlanWith = (extraResidentBytes) => planDs4ExpertCache({
|
|
311
|
+
configuredGb: config.ds4CacheExpertsGb,
|
|
312
|
+
catalogCacheBytes: ds4Source?.cacheExpertsBytes,
|
|
313
|
+
catalogResidentBytes: (ds4Source?.residentBytes ?? 0) + visionResidentBytes + extraResidentBytes,
|
|
314
|
+
totalRamBytes: totalRamGb * 1024 ** 3,
|
|
315
|
+
});
|
|
310
316
|
// DSpark speculative decoding. Resolved here, beside the residency decision
|
|
311
|
-
// it depends on: ds4 aborts at startup if an external `--mtp-model` is
|
|
312
|
-
// `--ssd-streaming
|
|
317
|
+
// it depends on: ds4 aborts at startup if an external `--mtp-model` is
|
|
318
|
+
// combined with `--ssd-streaming` for any family but DeepSeek V4.1.
|
|
319
|
+
const targetArchitecture = installedModel?.architecture;
|
|
320
|
+
const streamedDrafting = ssdStreaming && ds4DsparkStreamsWithSsd(targetArchitecture);
|
|
313
321
|
const dsparkSupportPath = config.ds4DsparkModelPath ?? installedModel?.draftModelPath;
|
|
314
322
|
// Residency is decided on the weights alone, then the companion is priced
|
|
315
323
|
// against what is left. That precedence is deliberate and not symmetric:
|
|
316
324
|
// residency is worth ~9.5x and drafting ~1.05x, so a companion that would
|
|
317
325
|
// push the model out of memory loses its own flag rather than costing the
|
|
318
|
-
// launch its residency.
|
|
326
|
+
// launch its residency. A streamed V4.1 launch keeps its drafter resident
|
|
327
|
+
// too, so it must still leave room for a minimum expert cache.
|
|
319
328
|
let companionFitsMemory = true;
|
|
320
329
|
let dsparkCompanionBytes = 0;
|
|
321
|
-
if (dsparkSupportPath && !ssdStreaming) {
|
|
330
|
+
if (dsparkSupportPath && (!ssdStreaming || streamedDrafting)) {
|
|
322
331
|
const { canUseDs4FullResidency } = await import('./residency.js');
|
|
323
332
|
const { stat: statCompanion } = await import('node:fs/promises');
|
|
324
333
|
const companionBytes = await statCompanion(dsparkSupportPath)
|
|
325
334
|
.then((st) => st.size)
|
|
326
335
|
.catch(() => undefined);
|
|
327
|
-
dsparkCompanionBytes =
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
:
|
|
334
|
-
|
|
336
|
+
dsparkCompanionBytes =
|
|
337
|
+
(companionBytes ?? 0) +
|
|
338
|
+
(ds4DsparkStreamsWithSsd(targetArchitecture) ? DS4_V41_DRAFTER_RUNTIME_BYTES : 0);
|
|
339
|
+
companionFitsMemory = ssdStreaming
|
|
340
|
+
? cachePlanWith(dsparkCompanionBytes).safe
|
|
341
|
+
: canUseDs4FullResidency({
|
|
342
|
+
modelSizeBytes: residentWeightBytes,
|
|
343
|
+
totalRamBytes,
|
|
344
|
+
...(companionBytes !== undefined || visionResidentBytes > 0
|
|
345
|
+
? { companionBytes: dsparkCompanionBytes + visionResidentBytes }
|
|
346
|
+
: {}),
|
|
347
|
+
});
|
|
335
348
|
}
|
|
336
349
|
const dspark = resolveDs4Dspark({
|
|
337
350
|
mode: config.ds4Dspark,
|
|
338
351
|
backend: backendFlag.slice(2),
|
|
339
352
|
ssdStreaming,
|
|
353
|
+
...(targetArchitecture ? { targetArchitecture } : {}),
|
|
340
354
|
companionFitsMemory,
|
|
341
355
|
...(dsparkSupportPath ? { supportModelPath: dsparkSupportPath } : {}),
|
|
342
356
|
});
|
|
@@ -365,14 +379,7 @@ export async function buildDs4Provider(opts) {
|
|
|
365
379
|
});
|
|
366
380
|
if (mtp.unmetRequest)
|
|
367
381
|
log.warn(`[ds4] ${mtp.unmetRequest}`);
|
|
368
|
-
const cachePlan =
|
|
369
|
-
configuredGb: config.ds4CacheExpertsGb,
|
|
370
|
-
catalogCacheBytes: ds4Source?.cacheExpertsBytes,
|
|
371
|
-
catalogResidentBytes: ds4Source?.residentBytes !== undefined || visionResidentBytes > 0
|
|
372
|
-
? (ds4Source?.residentBytes ?? 0) + visionResidentBytes
|
|
373
|
-
: undefined,
|
|
374
|
-
totalRamBytes: totalRamGb * 1024 ** 3,
|
|
375
|
-
});
|
|
382
|
+
const cachePlan = cachePlanWith(dspark.enabled && ssdStreaming ? dsparkCompanionBytes : 0);
|
|
376
383
|
if (ssdStreaming && !cachePlan.safe) {
|
|
377
384
|
const err = new Error(`DwarfStar (ds4) engine: ${effectiveModelId ?? 'the selected model'} cannot keep a safe minimum expert cache while preserving memory for the operating system. Choose a lighter model in Settings → DwarfStar (ds4).`);
|
|
378
385
|
err.isActionable = true;
|
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
* one measurement shape the policy here:
|
|
7
7
|
*
|
|
8
8
|
* - **The engine refuses an external MTP model alongside `--ssd-streaming`**,
|
|
9
|
-
* aborting at startup.
|
|
10
|
-
*
|
|
11
|
-
* on a 128 GiB machine, so
|
|
9
|
+
* aborting at startup, for every family except DeepSeek V4.1
|
|
10
|
+
* (`general.architecture` `deepseek41`, upstream ee7e840). Streaming is the
|
|
11
|
+
* only way the 153 GiB Q4 GGUF runs at all on a 128 GiB machine, so
|
|
12
|
+
* drafting is unreachable there. V4.1 keeps its 8.04 GiB drafter resident
|
|
13
|
+
* while the target's experts stream, but upstream warns it can still be
|
|
14
|
+
* slower than plain decoding, so `auto` never drafts a streamed launch.
|
|
12
15
|
* - **A support model is required.** Without one there is nothing to draft
|
|
13
16
|
* with, and ds4 exits on `--dspark` alone ("--dspark requires --mtp-model FILE").
|
|
14
17
|
* - **It does not pay on Metal.** Measured 2026-08-26, M5 Max, DeepSeek V4
|
|
@@ -25,6 +28,18 @@
|
|
|
25
28
|
*/
|
|
26
29
|
/** Which acceleration backend ds4 was launched with. */
|
|
27
30
|
export type Ds4Backend = 'metal' | 'cuda';
|
|
31
|
+
/**
|
|
32
|
+
* Whether ds4 accepts `--mtp-model` alongside `--ssd-streaming` for a target
|
|
33
|
+
* with this GGUF `general.architecture`. Mirrors the engine's own check
|
|
34
|
+
* (`DS4_MODEL_FAMILY != DS4_MODEL_FAMILY_DEEPSEEK41` in `ds4_engine_open`);
|
|
35
|
+
* an unknown architecture is treated as unsupported because the engine aborts.
|
|
36
|
+
*/
|
|
37
|
+
export declare function ds4DsparkStreamsWithSsd(architecture: string | undefined): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Buffers ds4 allocates beside a V4.1 drafter's weights; its startup memory
|
|
40
|
+
* plan charges `mtp_model.size + 64 MiB` for them.
|
|
41
|
+
*/
|
|
42
|
+
export declare const DS4_V41_DRAFTER_RUNTIME_BYTES: number;
|
|
28
43
|
export type Ds4DsparkMode = 'off' | 'on' | 'auto';
|
|
29
44
|
export interface Ds4DsparkOptions {
|
|
30
45
|
/** `config.ds4Dspark`. Absent → `auto`. */
|
|
@@ -33,12 +48,15 @@ export interface Ds4DsparkOptions {
|
|
|
33
48
|
backend: Ds4Backend;
|
|
34
49
|
/** Whether this launch streams experts from SSD. */
|
|
35
50
|
ssdStreaming: boolean;
|
|
51
|
+
/** The target GGUF's `general.architecture`, when known. */
|
|
52
|
+
targetArchitecture?: string;
|
|
36
53
|
/** Absolute path to a DSpark support GGUF, if one resolved. */
|
|
37
54
|
supportModelPath?: string;
|
|
38
55
|
/**
|
|
39
|
-
* Whether the companion still fits once the resident weights
|
|
56
|
+
* Whether the companion still fits once the resident weights (or, when
|
|
57
|
+
* streaming, the resident set plus a minimum expert cache) are accounted
|
|
40
58
|
* for. Defaults to true so callers that cannot weigh it keep the old
|
|
41
|
-
* behavior. When false the launch keeps its
|
|
59
|
+
* behavior. When false the launch keeps its memory plan and drops drafting —
|
|
42
60
|
* residency is worth ~9.5x and drafting ~1.05x, so the flag is what yields.
|
|
43
61
|
*/
|
|
44
62
|
companionFitsMemory?: boolean;
|
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
* one measurement shape the policy here:
|
|
7
7
|
*
|
|
8
8
|
* - **The engine refuses an external MTP model alongside `--ssd-streaming`**,
|
|
9
|
-
* aborting at startup.
|
|
10
|
-
*
|
|
11
|
-
* on a 128 GiB machine, so
|
|
9
|
+
* aborting at startup, for every family except DeepSeek V4.1
|
|
10
|
+
* (`general.architecture` `deepseek41`, upstream ee7e840). Streaming is the
|
|
11
|
+
* only way the 153 GiB Q4 GGUF runs at all on a 128 GiB machine, so
|
|
12
|
+
* drafting is unreachable there. V4.1 keeps its 8.04 GiB drafter resident
|
|
13
|
+
* while the target's experts stream, but upstream warns it can still be
|
|
14
|
+
* slower than plain decoding, so `auto` never drafts a streamed launch.
|
|
12
15
|
* - **A support model is required.** Without one there is nothing to draft
|
|
13
16
|
* with, and ds4 exits on `--dspark` alone ("--dspark requires --mtp-model FILE").
|
|
14
17
|
* - **It does not pay on Metal.** Measured 2026-08-26, M5 Max, DeepSeek V4
|
|
@@ -23,6 +26,20 @@
|
|
|
23
26
|
* residency-or-unified predicate would enable drafting on the one platform
|
|
24
27
|
* where it was measured to lose.
|
|
25
28
|
*/
|
|
29
|
+
/**
|
|
30
|
+
* Whether ds4 accepts `--mtp-model` alongside `--ssd-streaming` for a target
|
|
31
|
+
* with this GGUF `general.architecture`. Mirrors the engine's own check
|
|
32
|
+
* (`DS4_MODEL_FAMILY != DS4_MODEL_FAMILY_DEEPSEEK41` in `ds4_engine_open`);
|
|
33
|
+
* an unknown architecture is treated as unsupported because the engine aborts.
|
|
34
|
+
*/
|
|
35
|
+
export function ds4DsparkStreamsWithSsd(architecture) {
|
|
36
|
+
return architecture === 'deepseek41';
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Buffers ds4 allocates beside a V4.1 drafter's weights; its startup memory
|
|
40
|
+
* plan charges `mtp_model.size + 64 MiB` for them.
|
|
41
|
+
*/
|
|
42
|
+
export const DS4_V41_DRAFTER_RUNTIME_BYTES = 64 * 1024 ** 2;
|
|
26
43
|
/**
|
|
27
44
|
* Resolve whether this launch drafts, and with what.
|
|
28
45
|
*
|
|
@@ -47,24 +64,29 @@ export function resolveDs4Dspark(opts) {
|
|
|
47
64
|
: {}),
|
|
48
65
|
};
|
|
49
66
|
}
|
|
50
|
-
if (opts.ssdStreaming) {
|
|
67
|
+
if (opts.ssdStreaming && !ds4DsparkStreamsWithSsd(opts.targetArchitecture)) {
|
|
51
68
|
return {
|
|
52
69
|
enabled: false,
|
|
53
|
-
reason: 'SSD streaming is on; ds4 rejects --mtp-model with --ssd-streaming',
|
|
70
|
+
reason: 'SSD streaming is on; ds4 rejects --mtp-model with --ssd-streaming except for DeepSeek V4.1',
|
|
54
71
|
...(mode === 'on'
|
|
55
72
|
? {
|
|
56
|
-
unmetRequest: 'ds4Dspark=on cannot be honored while SSD streaming is active — ds4 refuses an external --mtp-model with --ssd-streaming. Full residency requires a model that fits this machine with headroom to spare (ds4SsdStreaming=false).',
|
|
73
|
+
unmetRequest: 'ds4Dspark=on cannot be honored while SSD streaming is active — ds4 refuses an external --mtp-model with --ssd-streaming for every model but DeepSeek V4.1. Full residency requires a model that fits this machine with headroom to spare (ds4SsdStreaming=false).',
|
|
57
74
|
}
|
|
58
75
|
: {}),
|
|
59
76
|
};
|
|
60
77
|
}
|
|
61
78
|
if (opts.companionFitsMemory === false) {
|
|
79
|
+
const held = opts.ssdStreaming
|
|
80
|
+
? 'the resident weights and a minimum expert cache'
|
|
81
|
+
: 'the resident weights';
|
|
62
82
|
return {
|
|
63
83
|
enabled: false,
|
|
64
|
-
reason:
|
|
84
|
+
reason: `support model does not fit alongside ${held}`,
|
|
65
85
|
...(mode === 'on'
|
|
66
86
|
? {
|
|
67
|
-
unmetRequest:
|
|
87
|
+
unmetRequest: opts.ssdStreaming
|
|
88
|
+
? `ds4Dspark=on cannot be honored: the DSpark support model does not fit in memory alongside ${held}. Drafting was dropped rather than starving the expert cache.`
|
|
89
|
+
: 'ds4Dspark=on cannot be honored: the DSpark support model does not fit in memory alongside the fully resident weights. Drafting was dropped rather than giving up full residency, which is worth far more than drafting.',
|
|
68
90
|
}
|
|
69
91
|
: {}),
|
|
70
92
|
};
|
|
@@ -77,6 +99,12 @@ export function resolveDs4Dspark(opts) {
|
|
|
77
99
|
};
|
|
78
100
|
}
|
|
79
101
|
// auto
|
|
102
|
+
if (opts.ssdStreaming) {
|
|
103
|
+
return {
|
|
104
|
+
enabled: false,
|
|
105
|
+
reason: 'auto: drafting a streamed launch is unmeasured and upstream reports it can be slower; set ds4Dspark=on to draft',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
80
108
|
if (opts.backend !== 'cuda') {
|
|
81
109
|
return {
|
|
82
110
|
enabled: false,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-model img2img capability resolution for the local sd-cpp engine.
|
|
3
|
+
*
|
|
4
|
+
* stable-diffusion.cpp's `/sdapi/v1/img2img` endpoint accepts
|
|
5
|
+
* `init_images` for any loaded model, but whether the *architecture*
|
|
6
|
+
* actually honors the init latents varies: the classic SD 1.x / SDXL
|
|
7
|
+
* denoising path is mature, while newer diffusion-model architectures
|
|
8
|
+
* (FLUX.2's LLM-encoder family, Krea 2's Qwen-Image lineage) are unverified on
|
|
9
|
+
* the pinned sd-server — the same server build that already ignores
|
|
10
|
+
* per-request step overrides for Krea (see MODEL_SAMPLE_STEP_DEFAULTS).
|
|
11
|
+
*
|
|
12
|
+
* Resolution ladder, most explicit wins:
|
|
13
|
+
* 1. `supportsImg2Img` declared on the catalog manifest (persisted
|
|
14
|
+
* into the installed `manifest.json` at pull time),
|
|
15
|
+
* 2. the assessment map below (repo-local truth for shipped models,
|
|
16
|
+
* same pattern as MODEL_CFG_DEFAULTS),
|
|
17
|
+
* 3. a `weightsKind` default: `checkpoint` (and unknown/legacy
|
|
18
|
+
* installs, which are all SD-era checkpoints) → supported;
|
|
19
|
+
* `diffusion-model` → not supported until assessed.
|
|
20
|
+
*
|
|
21
|
+
* When a model resolves unsupported, callers drop the input images and
|
|
22
|
+
* report `img2imgSkippedReason` instead of silently producing an
|
|
23
|
+
* "edit" that never saw the source image. Flip a map entry (or set the
|
|
24
|
+
* catalog field) after verifying a family on real hardware.
|
|
25
|
+
*/
|
|
26
|
+
export declare const MODEL_IMG2IMG_SUPPORT: Map<string, boolean>;
|
|
27
|
+
export interface Img2ImgSupportInput {
|
|
28
|
+
/** Resolved model id; undefined when nothing is installed. */
|
|
29
|
+
modelId?: string | undefined;
|
|
30
|
+
/** Explicit capability from catalog/installed metadata, when declared. */
|
|
31
|
+
explicit?: boolean | undefined;
|
|
32
|
+
/** Loading shape from installed metadata, when recorded. */
|
|
33
|
+
weightsKind?: 'checkpoint' | 'diffusion-model' | undefined;
|
|
34
|
+
}
|
|
35
|
+
export interface Img2ImgSupportVerdict {
|
|
36
|
+
supported: boolean;
|
|
37
|
+
/** Human-readable reason, set only when unsupported. */
|
|
38
|
+
reason?: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function resolveImg2ImgSupport(input: Img2ImgSupportInput): Img2ImgSupportVerdict;
|