@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.
Files changed (104) hide show
  1. package/dist/catalog/chat-models/index.json +1 -1
  2. package/dist/catalog/chat-models/na/nanbeige4.2-3b-q4/manifest.json +1 -1
  3. package/dist/catalog/chat-models/ne/nemotron3.5-bolt-e4b-q4/manifest.json +142 -0
  4. package/dist/catalog/chat-models/ne/nemotron3.5-bolt-e4b-q4/versions/1.0.0/manifest.json +14 -0
  5. package/dist/catalog/chat-models/ne/nemotron3.5-bolt-e4b-q4/versions/1.0.1/manifest.json +14 -0
  6. package/dist/catalog/chat-models/raw-index.json +1 -1
  7. package/dist/catalog/image-models/an/animagine-xl-4/manifest.json +40 -0
  8. package/dist/catalog/image-models/an/animagine-xl-4/versions/4.0.0/manifest.json +10 -0
  9. package/dist/catalog/image-models/an/animagine-xl-4/versions/4.0.1/manifest.json +16 -0
  10. package/dist/catalog/image-models/fl/flux-2-klein-4b-q4/manifest.json +44 -0
  11. package/dist/catalog/image-models/fl/flux-2-klein-4b-q4/versions/1.0.0/manifest.json +23 -0
  12. package/dist/catalog/image-models/fl/flux-2-klein-4b-q4/versions/1.0.1/manifest.json +29 -0
  13. package/dist/catalog/image-models/index.json +1 -0
  14. package/dist/catalog/image-models/ir/iris-3b-f32/manifest.json +41 -0
  15. package/dist/catalog/image-models/ir/iris-3b-f32/versions/1.0.0/manifest.json +19 -0
  16. package/dist/catalog/image-models/ir/iris-3b-f32/versions/1.0.1/manifest.json +25 -0
  17. package/dist/catalog/image-models/ir/iris-3b-f32/versions/1.0.2/manifest.json +25 -0
  18. package/dist/catalog/image-models/kr/krea-2-turbo-q4/manifest.json +42 -0
  19. package/dist/catalog/image-models/kr/krea-2-turbo-q4/versions/1.0.0/manifest.json +23 -0
  20. package/dist/catalog/image-models/kr/krea-2-turbo-q4/versions/1.0.1/manifest.json +29 -0
  21. package/dist/catalog/image-models/kr/krea-2-turbo-q6/manifest.json +42 -0
  22. package/dist/catalog/image-models/kr/krea-2-turbo-q6/versions/1.0.0/manifest.json +23 -0
  23. package/dist/catalog/image-models/kr/krea-2-turbo-q6/versions/1.0.1/manifest.json +29 -0
  24. package/dist/catalog/image-models/kr/krea-2-turbo-q8/manifest.json +42 -0
  25. package/dist/catalog/image-models/kr/krea-2-turbo-q8/versions/1.0.0/manifest.json +23 -0
  26. package/dist/catalog/image-models/kr/krea-2-turbo-q8/versions/1.0.1/manifest.json +29 -0
  27. package/dist/catalog/image-models/mi/ming-image-0.1-design-bf16/manifest.json +41 -0
  28. package/dist/catalog/image-models/mi/ming-image-0.1-design-bf16/versions/1.0.0/manifest.json +31 -0
  29. package/dist/catalog/image-models/mi/ming-image-0.1-design-bf16/versions/1.0.1/manifest.json +50 -0
  30. package/dist/catalog/image-models/mi/ming-image-0.1-design-bf16/versions/1.0.2/manifest.json +50 -0
  31. package/dist/catalog/image-models/mo/moss-image-sft-q4/manifest.json +40 -0
  32. package/dist/catalog/image-models/mo/moss-image-sft-q4/versions/1.0.0/manifest.json +37 -0
  33. package/dist/catalog/image-models/mo/moss-image-sft-q4/versions/1.0.1/manifest.json +37 -0
  34. package/dist/catalog/image-models/raw-index.json +1 -0
  35. package/dist/catalog/image-models/sd/sd-1.5/manifest.json +40 -0
  36. package/dist/catalog/image-models/sd/sd-1.5/versions/1.0.0/manifest.json +10 -0
  37. package/dist/catalog/image-models/sd/sd-1.5/versions/1.0.1/manifest.json +16 -0
  38. package/dist/catalog/image-models/sd/sd-turbo/manifest.json +40 -0
  39. package/dist/catalog/image-models/sd/sd-turbo/versions/1.0.0/manifest.json +10 -0
  40. package/dist/catalog/image-models/sd/sd-turbo/versions/1.0.1/manifest.json +16 -0
  41. package/dist/catalog/image-models/sd/sdxl-base-1.0/manifest.json +39 -0
  42. package/dist/catalog/image-models/sd/sdxl-base-1.0/versions/1.0.0/manifest.json +10 -0
  43. package/dist/catalog/image-models/sd/sdxl-base-1.0/versions/1.0.1/manifest.json +16 -0
  44. package/dist/catalog/image-models/sd/sdxl-lightning-4step/manifest.json +40 -0
  45. package/dist/catalog/image-models/sd/sdxl-lightning-4step/versions/1.0.0/manifest.json +10 -0
  46. package/dist/catalog/image-models/sd/sdxl-lightning-4step/versions/1.0.1/manifest.json +16 -0
  47. package/dist/catalog/image-models/sd/sdxl-turbo/manifest.json +41 -0
  48. package/dist/catalog/image-models/sd/sdxl-turbo/versions/1.0.0/manifest.json +10 -0
  49. package/dist/catalog/image-models/sd/sdxl-turbo/versions/1.0.1/manifest.json +16 -0
  50. package/dist/catalog/provenance.json +2 -1
  51. package/dist/engines/native-file-manifest.json +129 -119
  52. package/dist/engines/native-manifest.d.ts +4 -4
  53. package/dist/engines/native-manifest.js +36 -36
  54. package/dist/engines/resolver.js +9 -0
  55. package/dist/http/openai-compat/engine-failure.js +7 -2
  56. package/dist/http/openai-compat/responses-streaming.d.ts +13 -0
  57. package/dist/http/openai-compat/responses-streaming.js +30 -0
  58. package/dist/http/openai-compat/streaming.d.ts +9 -0
  59. package/dist/http/openai-compat/streaming.js +49 -9
  60. package/dist/http/openai-compat/tool-call-screen.d.ts +28 -0
  61. package/dist/http/openai-compat/tool-call-screen.js +43 -0
  62. package/dist/http/routes/v1-chat.js +15 -1
  63. package/dist/http/routes/v1-images.d.ts +44 -0
  64. package/dist/http/routes/v1-images.js +370 -0
  65. package/dist/http/routes/v1-responses.js +13 -1
  66. package/dist/inference-service.d.ts +3 -0
  67. package/dist/inference-service.js +24 -2
  68. package/dist/kokoro-lexicon/manifest.json +6 -3
  69. package/dist/models/image-fit.d.ts +83 -0
  70. package/dist/models/image-fit.js +163 -0
  71. package/dist/models/weight-facts.d.ts +36 -0
  72. package/dist/models/weight-facts.js +171 -0
  73. package/dist/providers/ds4/build-provider.js +28 -21
  74. package/dist/providers/ds4/dspark.d.ts +23 -5
  75. package/dist/providers/ds4/dspark.js +36 -8
  76. package/dist/providers/image/img2img-support.d.ts +40 -0
  77. package/dist/providers/image/img2img-support.js +74 -0
  78. package/dist/providers/image/local-engine.d.ts +31 -0
  79. package/dist/providers/image/local-engine.js +47 -0
  80. package/dist/providers/image/local-factory.d.ts +95 -0
  81. package/dist/providers/image/local-factory.js +270 -0
  82. package/dist/providers/image/mock.d.ts +23 -0
  83. package/dist/providers/image/mock.js +151 -0
  84. package/dist/providers/image/pull-registry.d.ts +106 -0
  85. package/dist/providers/image/pull-registry.js +227 -0
  86. package/dist/providers/image/sd-cpp.d.ts +200 -0
  87. package/dist/providers/image/sd-cpp.js +845 -0
  88. package/dist/providers/image/types.d.ts +237 -0
  89. package/dist/providers/image/types.js +20 -0
  90. package/dist/providers/llama-cpp/backend-health.d.ts +7 -0
  91. package/dist/providers/llama-cpp/backend-health.js +41 -1
  92. package/dist/providers/mlx/provider.js +13 -3
  93. package/dist/providers/native/supervisor.d.ts +8 -0
  94. package/dist/providers/native/supervisor.js +52 -4
  95. package/dist/providers/project-macro-loop-bail.js +5 -3
  96. package/dist/providers/provider-lifecycle.d.ts +21 -0
  97. package/dist/providers/provider-lifecycle.js +83 -0
  98. package/dist/providers/retirement-gate.d.ts +20 -0
  99. package/dist/providers/retirement-gate.js +55 -0
  100. package/dist/providers/types.d.ts +7 -1
  101. package/dist/system/memory.js +6 -0
  102. package/dist/utils/image-meta.d.ts +12 -0
  103. package/dist/utils/image-meta.js +77 -0
  104. package/package.json +6 -6
@@ -0,0 +1,151 @@
1
+ /**
2
+ * MockImageProvider — deterministic image generation for tests and for the
3
+ * `GEZEL_MOCK_PROVIDER=1` flow. Produces a tiny, valid PNG whose pixel
4
+ * colour is derived from a hash of the prompt so different prompts yield
5
+ * visually different (but still trivially small) outputs.
6
+ */
7
+ import { deflateSync } from 'node:zlib';
8
+ export class MockImageProvider {
9
+ name = 'mock';
10
+ installed = new Map();
11
+ /** Internal counter driving deterministic seed generation on "random" seeds. */
12
+ nextSeed = 1;
13
+ constructor(opts = {}) {
14
+ for (const m of opts.preinstalled ?? []) {
15
+ this.installed.set(m.id, m);
16
+ }
17
+ }
18
+ /** Number of input images received on the most recent generate(). Tests use this. */
19
+ lastInputImageCount = 0;
20
+ async generate(input) {
21
+ this.lastInputImageCount = input.inputImages?.length ?? 0;
22
+ const width = clamp(input.width ?? 16, 1, 512);
23
+ const height = clamp(input.height ?? 16, 1, 512);
24
+ const steps = clamp(input.steps ?? 1, 1, 100);
25
+ const seed = input.seed ?? this.nextSeed++;
26
+ const model = input.model ?? [...this.installed.keys()][0] ?? 'mock-model';
27
+ const rgba = solidFill(width, height, colourForPrompt(input.prompt));
28
+ const png = encodePng(rgba, width, height);
29
+ return {
30
+ png,
31
+ meta: {
32
+ model,
33
+ seed,
34
+ steps,
35
+ widthPx: width,
36
+ heightPx: height,
37
+ durationMs: 1,
38
+ },
39
+ };
40
+ }
41
+ async listInstalledModels() {
42
+ return [...this.installed.values()];
43
+ }
44
+ async *pullModel(id, spec, signal) {
45
+ const total = spec.approxSizeBytes;
46
+ for (const step of [0.25, 0.5, 0.75, 1]) {
47
+ if (signal?.aborted) {
48
+ yield { type: 'error', error: 'download aborted' };
49
+ yield { type: 'done', id };
50
+ return;
51
+ }
52
+ // Tiny tick so tests can observe a mid-pull state between steps.
53
+ await new Promise((resolve) => {
54
+ const t = setTimeout(resolve, 5);
55
+ t.unref?.();
56
+ });
57
+ yield { type: 'progress', bytesWritten: Math.round(total * step), totalBytes: total };
58
+ }
59
+ this.installed.set(id, {
60
+ id,
61
+ name: spec.name,
62
+ approxSizeBytes: total,
63
+ installedAt: new Date().toISOString(),
64
+ });
65
+ yield { type: 'done', id };
66
+ }
67
+ async deleteModel(id) {
68
+ this.installed.delete(id);
69
+ }
70
+ async health() {
71
+ return { status: 'ok', baseUrl: 'mock://image' };
72
+ }
73
+ }
74
+ function clamp(n, min, max) {
75
+ if (n < min)
76
+ return min;
77
+ if (n > max)
78
+ return max;
79
+ return n;
80
+ }
81
+ /** Hash the prompt to an `[r, g, b]` tuple so different prompts paint different pixels. */
82
+ function colourForPrompt(prompt) {
83
+ let h = 2166136261;
84
+ for (let i = 0; i < prompt.length; i++) {
85
+ h ^= prompt.charCodeAt(i);
86
+ h = Math.imul(h, 16777619);
87
+ }
88
+ return [h & 0xff, (h >>> 8) & 0xff, (h >>> 16) & 0xff];
89
+ }
90
+ function solidFill(width, height, rgb) {
91
+ const rgba = new Uint8Array(width * height * 4);
92
+ for (let i = 0; i < width * height; i++) {
93
+ rgba[i * 4] = rgb[0];
94
+ rgba[i * 4 + 1] = rgb[1];
95
+ rgba[i * 4 + 2] = rgb[2];
96
+ rgba[i * 4 + 3] = 0xff;
97
+ }
98
+ return rgba;
99
+ }
100
+ // ── Minimal PNG encoder (RGBA, no filter) ──────────────────────────
101
+ const CRC_TABLE = (() => {
102
+ const table = new Uint32Array(256);
103
+ for (let n = 0; n < 256; n++) {
104
+ let c = n;
105
+ for (let k = 0; k < 8; k++)
106
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
107
+ table[n] = c >>> 0;
108
+ }
109
+ return table;
110
+ })();
111
+ function crc32(bytes) {
112
+ let crc = 0xffffffff;
113
+ for (let i = 0; i < bytes.length; i++)
114
+ crc = CRC_TABLE[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8);
115
+ return (crc ^ 0xffffffff) >>> 0;
116
+ }
117
+ function chunk(type, data) {
118
+ const typeBytes = Buffer.from(type, 'ascii');
119
+ const buf = Buffer.alloc(8 + data.length + 4);
120
+ buf.writeUInt32BE(data.length, 0);
121
+ typeBytes.copy(buf, 4);
122
+ Buffer.from(data).copy(buf, 8);
123
+ const crcInput = new Uint8Array(typeBytes.length + data.length);
124
+ crcInput.set(typeBytes, 0);
125
+ crcInput.set(data, typeBytes.length);
126
+ buf.writeUInt32BE(crc32(crcInput), 8 + data.length);
127
+ return buf;
128
+ }
129
+ function encodePng(rgba, width, height) {
130
+ const sig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
131
+ const ihdr = Buffer.alloc(13);
132
+ ihdr.writeUInt32BE(width, 0);
133
+ ihdr.writeUInt32BE(height, 4);
134
+ ihdr[8] = 8;
135
+ ihdr[9] = 6;
136
+ ihdr[10] = 0;
137
+ ihdr[11] = 0;
138
+ ihdr[12] = 0;
139
+ const raw = new Uint8Array(height * (1 + width * 4));
140
+ for (let y = 0; y < height; y++) {
141
+ raw[y * (1 + width * 4)] = 0;
142
+ raw.set(rgba.subarray(y * width * 4, (y + 1) * width * 4), y * (1 + width * 4) + 1);
143
+ }
144
+ const idat = deflateSync(raw);
145
+ return Buffer.concat([
146
+ sig,
147
+ chunk('IHDR', ihdr),
148
+ chunk('IDAT', idat),
149
+ chunk('IEND', new Uint8Array(0)),
150
+ ]);
151
+ }
@@ -0,0 +1,106 @@
1
+ import { type ImageModelPullEvent } from '@bendyline/gezel';
2
+ import type { CatalogService } from '@bendyline/gezel-catalog';
3
+ import type { ImageProvider } from './types.js';
4
+ /**
5
+ * Snapshot of one in-flight (or recently-finished) image-model pull.
6
+ * Exposed to the UI via `GET /api/image-gen/pulls` so a client that
7
+ * mounted after the pull started can still render an accurate
8
+ * progress bar without subscribing to the live SSE.
9
+ */
10
+ export interface ActiveImagePull {
11
+ id: string;
12
+ startedAt: string;
13
+ bytesWritten: number;
14
+ /** Best-known total. Zero until the first `progress` event arrives. */
15
+ totalBytes: number;
16
+ /** Present when the shared downloader is between retry attempts. */
17
+ retrying?: {
18
+ attempt: number;
19
+ maxAttempts: number;
20
+ delayMs: number;
21
+ reason: string;
22
+ };
23
+ /** Set once a terminal event has been observed (done or error). */
24
+ finished: boolean;
25
+ /** Set when the pull errored — kept on the snapshot so reconnecting clients see the failure. */
26
+ error?: string;
27
+ }
28
+ type Listener = (event: ImageModelPullEvent) => void;
29
+ /**
30
+ * Where pulls get their provider: the service's routing manager, or the
31
+ * hosted inference's local provider. Read on every pull so a provider reset
32
+ * (an engine binary swap) is picked up.
33
+ */
34
+ export interface ImageProviderSource {
35
+ current(): Promise<Pick<ImageProvider, 'pullModel'>>;
36
+ }
37
+ /**
38
+ * Owns the lifecycle of every in-flight image-model pull.
39
+ *
40
+ * Pulls used to be tied 1:1 to the HTTP request that started them — when
41
+ * the client disconnected (navigated away from Settings → Image
42
+ * generation), the SSE loop exited, the async generator closed, and the
43
+ * download was aborted by the .partial-leaves-resumable code path. A
44
+ * returning client had no record the pull ever existed.
45
+ *
46
+ * The registry decouples those two: the pull runs in the background owned
47
+ * by this object, and HTTP requests are just consumers that subscribe to
48
+ * the live event stream + the cached snapshot. Disconnect detaches the
49
+ * consumer; the pull keeps going. Cancel is now an explicit `DELETE
50
+ * /pulls/:id` rather than a side-effect of closing the fetch.
51
+ */
52
+ export declare class ImageModelPullRegistry {
53
+ private readonly entries;
54
+ private readonly controllers;
55
+ private readonly imageProvider;
56
+ private readonly catalog;
57
+ constructor(opts: {
58
+ imageProvider: ImageProviderSource;
59
+ catalog: CatalogService;
60
+ });
61
+ /**
62
+ * Start a pull for `id`. Idempotent — when a pull is already in flight
63
+ * for this id, returns the existing snapshot and does not kick off a
64
+ * second download. Throws if the catalog id is unknown.
65
+ *
66
+ * The pull itself runs in a detached `void` task; this method returns
67
+ * synchronously-claimed state so callers can immediately subscribe
68
+ * without racing the first event.
69
+ */
70
+ start(id: string): Promise<{
71
+ snapshot: ActiveImagePull;
72
+ alreadyRunning: boolean;
73
+ }>;
74
+ list(): ActiveImagePull[];
75
+ get(id: string): ActiveImagePull | null;
76
+ /**
77
+ * Subscribe to live events for an in-flight pull. On subscribe, the
78
+ * registry immediately delivers a synthetic event reflecting the last
79
+ * known snapshot — without it, a late subscriber to a paused (between
80
+ * retry attempts) or just-started pull would see nothing until the
81
+ * next real event, leaving the UI's progress bar blank.
82
+ *
83
+ * Returns `null` when no entry exists for `id` so the caller can
84
+ * 404. Returns an unsubscribe function otherwise; calling it removes
85
+ * the listener but does NOT cancel the pull — disconnect ≠ cancel.
86
+ */
87
+ subscribe(id: string, listener: Listener): (() => void) | null;
88
+ /**
89
+ * Cancel an in-flight pull. The shared downloader observes the abort,
90
+ * leaves the `.partial` in place for resume, and surfaces an `error`
91
+ * event whose snapshot stays in the registry for `FINISHED_TTL_MS`.
92
+ * Returns true when a live entry was aborted.
93
+ */
94
+ cancel(id: string): boolean;
95
+ /** Test-only: drop everything so a fresh test starts clean. */
96
+ clear(): void;
97
+ private replaySnapshot;
98
+ private consume;
99
+ private applyEvent;
100
+ private broadcast;
101
+ }
102
+ export declare class UnknownImageModelError extends Error {
103
+ readonly id: string;
104
+ constructor(id: string);
105
+ }
106
+ export {};
@@ -0,0 +1,227 @@
1
+ import { createLogger } from '@bendyline/gezel';
2
+ const log = createLogger('image-pulls');
3
+ /**
4
+ * How long a finished pull (done OR error) stays in the registry after
5
+ * the terminal event. Long enough that a client polling once per second
6
+ * sees the final state on reconnect; short enough that the catalog UI's
7
+ * "Installed" row gets to take over.
8
+ */
9
+ const FINISHED_TTL_MS = 8_000;
10
+ /**
11
+ * Owns the lifecycle of every in-flight image-model pull.
12
+ *
13
+ * Pulls used to be tied 1:1 to the HTTP request that started them — when
14
+ * the client disconnected (navigated away from Settings → Image
15
+ * generation), the SSE loop exited, the async generator closed, and the
16
+ * download was aborted by the .partial-leaves-resumable code path. A
17
+ * returning client had no record the pull ever existed.
18
+ *
19
+ * The registry decouples those two: the pull runs in the background owned
20
+ * by this object, and HTTP requests are just consumers that subscribe to
21
+ * the live event stream + the cached snapshot. Disconnect detaches the
22
+ * consumer; the pull keeps going. Cancel is now an explicit `DELETE
23
+ * /pulls/:id` rather than a side-effect of closing the fetch.
24
+ */
25
+ export class ImageModelPullRegistry {
26
+ entries = new Map();
27
+ controllers = new Map();
28
+ imageProvider;
29
+ catalog;
30
+ constructor(opts) {
31
+ this.imageProvider = opts.imageProvider;
32
+ this.catalog = opts.catalog;
33
+ }
34
+ /**
35
+ * Start a pull for `id`. Idempotent — when a pull is already in flight
36
+ * for this id, returns the existing snapshot and does not kick off a
37
+ * second download. Throws if the catalog id is unknown.
38
+ *
39
+ * The pull itself runs in a detached `void` task; this method returns
40
+ * synchronously-claimed state so callers can immediately subscribe
41
+ * without racing the first event.
42
+ */
43
+ async start(id) {
44
+ const existing = this.entries.get(id);
45
+ if (existing && !existing.snapshot.finished) {
46
+ return { snapshot: { ...existing.snapshot }, alreadyRunning: true };
47
+ }
48
+ // Resolve the catalog entry up-front so we can fail fast with a
49
+ // proper 404 rather than emitting a 200 + error SSE for "unknown id".
50
+ const item = await this.catalog.get('image-model', id);
51
+ if (!item || item.manifest.kind !== 'image-model') {
52
+ throw new UnknownImageModelError(id);
53
+ }
54
+ const manifest = item.manifest;
55
+ const snapshot = {
56
+ id,
57
+ startedAt: new Date().toISOString(),
58
+ bytesWritten: 0,
59
+ totalBytes: manifest.approxSizeBytes +
60
+ manifest.auxiliaryFiles.reduce((n, f) => n + f.approxSizeBytes, 0),
61
+ finished: false,
62
+ };
63
+ const entry = { snapshot, listeners: new Set() };
64
+ this.entries.set(id, entry);
65
+ const controller = new AbortController();
66
+ this.controllers.set(id, controller);
67
+ const spec = {
68
+ downloadUrl: manifest.downloadUrl,
69
+ sha256: manifest.sha256,
70
+ approxSizeBytes: manifest.approxSizeBytes,
71
+ name: manifest.name,
72
+ weightsKind: manifest.weightsKind,
73
+ ...(manifest.supportsImg2Img !== undefined
74
+ ? { supportsImg2Img: manifest.supportsImg2Img }
75
+ : {}),
76
+ auxiliaryFiles: manifest.auxiliaryFiles.map((aux) => ({
77
+ role: aux.role,
78
+ downloadUrl: aux.downloadUrl,
79
+ sha256: aux.sha256,
80
+ approxSizeBytes: aux.approxSizeBytes,
81
+ })),
82
+ };
83
+ void this.consume(id, spec, controller.signal);
84
+ return { snapshot: { ...snapshot }, alreadyRunning: false };
85
+ }
86
+ list() {
87
+ return Array.from(this.entries.values()).map((e) => ({ ...e.snapshot }));
88
+ }
89
+ get(id) {
90
+ const entry = this.entries.get(id);
91
+ return entry ? { ...entry.snapshot } : null;
92
+ }
93
+ /**
94
+ * Subscribe to live events for an in-flight pull. On subscribe, the
95
+ * registry immediately delivers a synthetic event reflecting the last
96
+ * known snapshot — without it, a late subscriber to a paused (between
97
+ * retry attempts) or just-started pull would see nothing until the
98
+ * next real event, leaving the UI's progress bar blank.
99
+ *
100
+ * Returns `null` when no entry exists for `id` so the caller can
101
+ * 404. Returns an unsubscribe function otherwise; calling it removes
102
+ * the listener but does NOT cancel the pull — disconnect ≠ cancel.
103
+ */
104
+ subscribe(id, listener) {
105
+ const entry = this.entries.get(id);
106
+ if (!entry)
107
+ return null;
108
+ entry.listeners.add(listener);
109
+ // Replay the current state so the UI renders immediately.
110
+ this.replaySnapshot(entry.snapshot, listener);
111
+ return () => {
112
+ entry.listeners.delete(listener);
113
+ };
114
+ }
115
+ /**
116
+ * Cancel an in-flight pull. The shared downloader observes the abort,
117
+ * leaves the `.partial` in place for resume, and surfaces an `error`
118
+ * event whose snapshot stays in the registry for `FINISHED_TTL_MS`.
119
+ * Returns true when a live entry was aborted.
120
+ */
121
+ cancel(id) {
122
+ const controller = this.controllers.get(id);
123
+ const entry = this.entries.get(id);
124
+ if (!controller || !entry || entry.snapshot.finished)
125
+ return false;
126
+ controller.abort();
127
+ return true;
128
+ }
129
+ /** Test-only: drop everything so a fresh test starts clean. */
130
+ clear() {
131
+ for (const controller of this.controllers.values())
132
+ controller.abort();
133
+ this.controllers.clear();
134
+ this.entries.clear();
135
+ }
136
+ replaySnapshot(snapshot, listener) {
137
+ // The order matters: deliver any in-progress retry banner LAST so it
138
+ // overrides the bare progress bar in the UI's state machine.
139
+ listener({
140
+ type: 'progress',
141
+ bytesWritten: snapshot.bytesWritten,
142
+ ...(snapshot.totalBytes > 0 ? { totalBytes: snapshot.totalBytes } : {}),
143
+ });
144
+ if (snapshot.retrying)
145
+ listener({ type: 'retrying', ...snapshot.retrying });
146
+ if (snapshot.error)
147
+ listener({ type: 'error', error: snapshot.error });
148
+ if (snapshot.finished && !snapshot.error)
149
+ listener({ type: 'done', id: snapshot.id });
150
+ }
151
+ async consume(id, spec, signal) {
152
+ const entry = this.entries.get(id);
153
+ if (!entry)
154
+ return;
155
+ try {
156
+ const provider = await this.imageProvider.current();
157
+ for await (const event of provider.pullModel(id, spec, signal)) {
158
+ this.applyEvent(entry, event);
159
+ this.broadcast(entry, event);
160
+ if (event.type === 'done' || event.type === 'error')
161
+ break;
162
+ }
163
+ }
164
+ catch (err) {
165
+ const message = err instanceof Error ? err.message : String(err);
166
+ const errorEvent = { type: 'error', error: message };
167
+ this.applyEvent(entry, errorEvent);
168
+ this.broadcast(entry, errorEvent);
169
+ log.warn(`[pull-registry] ${id} crashed: ${message}`);
170
+ }
171
+ finally {
172
+ entry.snapshot.finished = true;
173
+ // Drop the controller — cancel() should be a no-op once finished.
174
+ this.controllers.delete(id);
175
+ // Keep the snapshot around briefly so a client mid-reconnect still
176
+ // sees the terminal state on `list()` / `subscribe()`. Then garbage
177
+ // collect.
178
+ const timer = setTimeout(() => {
179
+ if (this.entries.get(id) === entry)
180
+ this.entries.delete(id);
181
+ }, FINISHED_TTL_MS);
182
+ timer.unref?.();
183
+ }
184
+ }
185
+ applyEvent(entry, event) {
186
+ const s = entry.snapshot;
187
+ if (event.type === 'progress') {
188
+ s.bytesWritten = event.bytesWritten;
189
+ if (event.totalBytes !== undefined)
190
+ s.totalBytes = event.totalBytes;
191
+ delete s.retrying;
192
+ }
193
+ else if (event.type === 'retrying') {
194
+ s.retrying = {
195
+ attempt: event.attempt,
196
+ maxAttempts: event.maxAttempts,
197
+ delayMs: event.delayMs,
198
+ reason: event.reason,
199
+ };
200
+ }
201
+ else if (event.type === 'error') {
202
+ s.error = event.error;
203
+ delete s.retrying;
204
+ }
205
+ else if (event.type === 'done') {
206
+ delete s.retrying;
207
+ }
208
+ }
209
+ broadcast(entry, event) {
210
+ for (const listener of entry.listeners) {
211
+ try {
212
+ listener(event);
213
+ }
214
+ catch (err) {
215
+ log.warn(`[pull-registry] listener threw: ${err instanceof Error ? err.message : err}`);
216
+ }
217
+ }
218
+ }
219
+ }
220
+ export class UnknownImageModelError extends Error {
221
+ id;
222
+ constructor(id) {
223
+ super(`unknown image-model: ${id}`);
224
+ this.id = id;
225
+ this.name = 'UnknownImageModelError';
226
+ }
227
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * StableDiffusionCppProvider — talks to a local `sd-server` binary over
3
+ * HTTP. The server speaks an OpenAI-compatible `/v1/images/generations`
4
+ * shape with a few extensions (seed, sample_steps, negative_prompt,
5
+ * cfg_scale) supported by upstream sd.cpp.
6
+ *
7
+ * Lifecycle of the server itself is not the provider's concern — an
8
+ * Electron-side supervisor (Phase 3) spawns / idle-stops the binary and
9
+ * the provider simply hits whichever URL is configured. If the server
10
+ * isn't reachable, {@link generate} surfaces a clear error so the UI
11
+ * can prompt the user to enable image generation in Settings.
12
+ *
13
+ * Model management is filesystem-only: weights live under
14
+ * `<modelsRoot>/<id>/weights.*` alongside a resolved copy of the
15
+ * catalog manifest (so local state is self-describing even if the
16
+ * catalog entry later disappears).
17
+ */
18
+ import { type ModelStorageRoots } from '../../models/storage-roots.js';
19
+ import type { GpuArbiter } from '../gpu-arbiter.js';
20
+ import type { NativeEngineSupervisor } from '../native/supervisor.js';
21
+ import { type ImageEngineHealth, type ImageGenerationInput, type ImageGenerationOutput, type ImageGenerationProgress, type ImageModelPullEvent, type ImageModelPullSpec, type ImageProvider, type InstalledImageModelInfo } from './types.js';
22
+ export interface StableDiffusionCppProviderOptions {
23
+ /** Base URL of the running sd-server (no trailing slash). */
24
+ baseUrl: string;
25
+ /** Absolute path to `~/.gezel/engines/sd-cpp/models`. */
26
+ modelsRoot: string;
27
+ /** Optional read-only machine-store overlay. */
28
+ storageRoots?: ModelStorageRoots;
29
+ /** Awake-time budget for one render request, in ms. Defaults to 30 minutes. */
30
+ timeoutMs?: number;
31
+ /** Injected for tests. Defaults to global fetch. */
32
+ fetchImpl?: typeof fetch;
33
+ /**
34
+ * Optional lifecycle manager. When supplied, the provider calls
35
+ * `ensureRunning()` before each generate and uses the returned
36
+ * baseUrl (overriding `baseUrl`) — so lazy-start / idle-stop /
37
+ * health-watch all work transparently. Omit for "user runs
38
+ * sd-server themselves" deployments.
39
+ */
40
+ supervisor?: NativeEngineSupervisor;
41
+ /**
42
+ * Whether the factory had a real engine wiring (an explicit URL or a
43
+ * spawnable binary) at construction. The default-loopback fallback
44
+ * sets this to false so `health()` can return `not-configured`
45
+ * instead of `unreachable` — distinct UI state, distinct guidance.
46
+ */
47
+ configured?: boolean;
48
+ /**
49
+ * Cross-engine GPU coordinator. When supplied alongside a
50
+ * supervisor, the provider acquires the `'image'` slot before each
51
+ * generate (which evicts the local LLM in `swap` mode) and
52
+ * registers its supervisor's `stop()` as the `'image'` evictor so
53
+ * the LLM can evict us back symmetrically.
54
+ */
55
+ arbiter?: GpuArbiter;
56
+ /**
57
+ * Shared, mutable holder the supervised path uses to tell the
58
+ * factory's `resolveLaunch` which installed model to spawn sd-server
59
+ * with. sd-server serves one model per process, so when a request
60
+ * targets a different installed model than the one loaded, the
61
+ * provider updates `modelId` here and stops the running server — the
62
+ * next `ensureRunning()` relaunches with the requested model instead
63
+ * of always serving whichever model sorts first.
64
+ */
65
+ launchState?: {
66
+ modelId: string | undefined;
67
+ };
68
+ /**
69
+ * The user's configured default model (`config.defaultImageModel['sd-cpp']`,
70
+ * Settings -> Image generation). Read on every generate that names no model
71
+ * of its own. A configured id that is no longer installed falls through to
72
+ * the first installed model, which is what every install did before this
73
+ * setting existed.
74
+ */
75
+ defaultModelId?: string;
76
+ }
77
+ /**
78
+ * Model-specific step counts, from fast distilled models to the 50 of MOSS
79
+ * and Iris. Shared with `buildSdServerArgs`, which sets
80
+ * the same server launch default for endpoints that ignore request steps.
81
+ */
82
+ export declare const MODEL_SAMPLE_STEP_DEFAULTS: Map<string, number>;
83
+ /**
84
+ * Classifier-free guidance (CFG) scale per model, applied to the
85
+ * `/sdapi/v1/txt2img` request. Distilled / Flux-flow models are trained
86
+ * to run at CFG 1 — their guidance is baked in (turbo/lightning) or
87
+ * carried by the separate Flux distilled-guidance value. Applying
88
+ * sd-server's higher default CFG to them blows out color and contrast
89
+ * (the oversaturated, "burned" look Krea 2 Turbo had before this).
90
+ * Models absent from the map (traditional SD 1.x / SDXL checkpoints)
91
+ * omit `cfg_scale` and inherit the server default, preserving their
92
+ * established behavior.
93
+ */
94
+ export declare const MODEL_CFG_DEFAULTS: Map<string, number>;
95
+ /**
96
+ * Whether the engine runs this model with guidance that reads a negative
97
+ * prompt. At CFG 1 the unconditional pass that carries it is skipped.
98
+ * Models absent from the map run at the server's default of 7.
99
+ */
100
+ export declare function sdCppHonorsNegativePrompt(modelId: string): boolean;
101
+ /** Pin samplers validated with the native engine for these models. */
102
+ export declare const MODEL_SAMPLER_DEFAULTS: Map<string, string>;
103
+ /**
104
+ * Models whose VAE shows visible tile-boundary seams under
105
+ * stable-diffusion.cpp's `--vae-tiling` (Krea 2's Qwen-Image VAE is the
106
+ * known case — rainbow column streaks at ≤512px). `--vae-tiling` is a
107
+ * launch-time, size-agnostic flag: sd-server serves every request size
108
+ * from one process, so we can't gate it per-request on image size as
109
+ * we'd ideally like — we gate it per-model instead and skip it here.
110
+ * The trade-off is that very large renders of these models could hit a
111
+ * VAE allocation cap on memory-constrained GPUs; acceptable given these
112
+ * are high-VRAM models and seam-free output matters more.
113
+ */
114
+ export declare const VAE_TILING_INCOMPATIBLE_MODELS: Set<string>;
115
+ /**
116
+ * The launch choices that change what sd-server holds in memory for one
117
+ * model. Read by the launch argv and by memory pricing, so the price is for
118
+ * the process that actually starts.
119
+ */
120
+ export interface SdServerLoadProfile {
121
+ vaeDecode: 'tiled' | 'untiled' | 'none';
122
+ /** The default render size, which working memory follows more than the weights do. */
123
+ defaultSidePx: number;
124
+ /** A `--tensor-type-rules` pattern whose tensors load as bfloat16. */
125
+ bf16TensorPattern?: string;
126
+ diffusionFlashAttention: boolean;
127
+ /**
128
+ * Reuse a diffusion transformer's output across steps whose input barely
129
+ * changed (`--cache-mode easycache`). Only for a model whose renders it was
130
+ * measured not to change.
131
+ */
132
+ stepCache?: 'easycache';
133
+ }
134
+ export declare function sdServerLoadProfile(modelId: string): SdServerLoadProfile;
135
+ export declare class StableDiffusionCppProvider implements ImageProvider {
136
+ readonly name = "sd-cpp";
137
+ private readonly baseUrl;
138
+ private readonly modelsRoot;
139
+ private readonly storageRoots;
140
+ private readonly timeoutMs;
141
+ private readonly fetchImpl;
142
+ private readonly supervisor?;
143
+ private readonly configured;
144
+ private readonly arbiter?;
145
+ private readonly launchState?;
146
+ private readonly defaultModelId?;
147
+ private renderTail;
148
+ private pendingRenders;
149
+ constructor(opts: StableDiffusionCppProviderOptions);
150
+ generate(input: ImageGenerationInput): Promise<ImageGenerationOutput>;
151
+ private generateInner;
152
+ listInstalledModels(): Promise<InstalledImageModelInfo[]>;
153
+ pullModel(id: string, spec: ImageModelPullSpec, signal?: AbortSignal): AsyncIterable<ImageModelPullEvent>;
154
+ /**
155
+ * Stream a single URL to disk with sha256 verification, reporting cumulative
156
+ * progress against `totalAllBytes` so multi-file pulls share one bar.
157
+ *
158
+ * Delegates the network plumbing to `downloadWithRetry` — that handles
159
+ * Range-based resume, exponential backoff, chunk-stall timeouts, and
160
+ * friendly error messages. This wrapper only owns the
161
+ * provider-specific concerns: sha256 verification, the
162
+ * `writtenSoFar`-aware progress translation, and the atomic rename
163
+ * after verify.
164
+ */
165
+ private downloadFile;
166
+ deleteModel(id: string): Promise<void>;
167
+ health(): Promise<ImageEngineHealth>;
168
+ shutdown(): Promise<void>;
169
+ /**
170
+ * The configured default when it is installed, else the first installed
171
+ * model id, else undefined. Stable ordering matches
172
+ * {@link listInstalledModels} so the default is deterministic.
173
+ */
174
+ resolveDefaultModelId(): Promise<string | undefined>;
175
+ private defaultSampleSteps;
176
+ /**
177
+ * Read the installed `manifest.json` fields that steer per-model
178
+ * behavior. Returns null for unknown ids and unreadable/legacy
179
+ * metadata — callers fall back to their per-field defaults. (Older
180
+ * installed metadata did not persist catalog generation defaults or
181
+ * the img2img capability.)
182
+ */
183
+ private readInstalledManifestFields;
184
+ }
185
+ /**
186
+ * Parse one line of sd-server's weight-load bar, which it prints inside the
187
+ * first request after a launch because it loads weights lazily:
188
+ *
189
+ * |######### | 49/298 - 435.57MB/s
190
+ */
191
+ export declare function parseWeightLoadProgress(line: string): ImageGenerationProgress | null;
192
+ /**
193
+ * Classify sd-server's progress bars into phases. Weight loads print a
194
+ * bytes-per-second bar. The first iteration bar is sampling; once it
195
+ * completes, the next is the tiled VAE decode, which reports tiles rather
196
+ * than steps. The step count the engine prints is used rather than the one
197
+ * requested, because some launch defaults override a request's steps.
198
+ */
199
+ export declare function progressTracker(emit: (progress: ImageGenerationProgress) => void): (line: string) => void;
200
+ export declare function parseSamplingProgress(line: string): ImageGenerationProgress | null;