@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,370 @@
|
|
|
1
|
+
import { AppImageGenerationRequestSchema, createLogger, imageModelDefaultSide, resolveImageSize, } from '@bendyline/gezel';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { streamSSE } from 'hono/streaming';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { imageCatalogFits, imageModelFit, wontRunRefusal } from '../../models/image-fit.js';
|
|
6
|
+
import { ReadOnlyModelError, modelExistsOnlyReadOnly, modelStorageRoots, } from '../../models/storage-roots.js';
|
|
7
|
+
import { NoLocalImageModelError } from '../../providers/image/local-factory.js';
|
|
8
|
+
import { ImageCapacityError, ImageGenerationCancelledError, } from '../../providers/image/types.js';
|
|
9
|
+
const log = createLogger('v1-images');
|
|
10
|
+
/** The one local image engine this host serves. */
|
|
11
|
+
const ENGINE = 'sd-cpp';
|
|
12
|
+
const EnsureRequestSchema = z.object({ model: z.string().min(1, 'model is required') });
|
|
13
|
+
function apiError(code, message) {
|
|
14
|
+
return { error: { message, type: 'invalid_request_error', code } };
|
|
15
|
+
}
|
|
16
|
+
/** `sd-cpp:<id>` or a bare `<id>`; null for another engine's id. */
|
|
17
|
+
function catalogIdOf(model) {
|
|
18
|
+
const trimmed = model.trim();
|
|
19
|
+
const colon = trimmed.indexOf(':');
|
|
20
|
+
if (colon < 0)
|
|
21
|
+
return trimmed || null;
|
|
22
|
+
if (trimmed.slice(0, colon) !== ENGINE)
|
|
23
|
+
return null;
|
|
24
|
+
return trimmed.slice(colon + 1).trim() || null;
|
|
25
|
+
}
|
|
26
|
+
function qualified(catalogId) {
|
|
27
|
+
return `${ENGINE}:${catalogId}`;
|
|
28
|
+
}
|
|
29
|
+
function downloadBytes(manifest) {
|
|
30
|
+
return (manifest.approxSizeBytes + manifest.auxiliaryFiles.reduce((n, f) => n + f.approxSizeBytes, 0));
|
|
31
|
+
}
|
|
32
|
+
function describeManifest(manifest) {
|
|
33
|
+
const license = manifest.licenseShortName ?? manifest.license;
|
|
34
|
+
return {
|
|
35
|
+
name: manifest.name,
|
|
36
|
+
description: manifest.description,
|
|
37
|
+
download_bytes: downloadBytes(manifest),
|
|
38
|
+
...(license ? { license } : {}),
|
|
39
|
+
commercial_use: manifest.commercialUse,
|
|
40
|
+
...(manifest.rendersText !== undefined ? { renders_text: manifest.rendersText } : {}),
|
|
41
|
+
...(manifest.promptStyle ? { prompt_style: manifest.promptStyle } : {}),
|
|
42
|
+
...(manifest.hardwareTier ? { hardware_tier: manifest.hardwareTier } : {}),
|
|
43
|
+
...(manifest.minRamGB !== undefined ? { min_ram_gb: manifest.minRamGB } : {}),
|
|
44
|
+
...(manifest.widthRange ? { width_range: manifest.widthRange } : {}),
|
|
45
|
+
...(manifest.heightRange ? { height_range: manifest.heightRange } : {}),
|
|
46
|
+
default_side_px: imageModelDefaultSide(manifest.id),
|
|
47
|
+
...(manifest.supportsImg2Img !== undefined
|
|
48
|
+
? { supports_img2img: manifest.supportsImg2Img }
|
|
49
|
+
: {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function ensureEvent(jobId, event, totalBytes) {
|
|
53
|
+
const job = { jobId, modelId: qualified(jobId) };
|
|
54
|
+
switch (event.type) {
|
|
55
|
+
case 'progress':
|
|
56
|
+
return {
|
|
57
|
+
...job,
|
|
58
|
+
type: 'progress',
|
|
59
|
+
bytesWritten: event.bytesWritten,
|
|
60
|
+
totalBytes: event.totalBytes ?? totalBytes,
|
|
61
|
+
};
|
|
62
|
+
case 'retrying':
|
|
63
|
+
return {
|
|
64
|
+
...job,
|
|
65
|
+
type: 'retrying',
|
|
66
|
+
attempt: event.attempt,
|
|
67
|
+
maxAttempts: event.maxAttempts,
|
|
68
|
+
delayMs: event.delayMs,
|
|
69
|
+
reason: event.reason,
|
|
70
|
+
};
|
|
71
|
+
case 'error':
|
|
72
|
+
return { ...job, type: 'error', error: event.error };
|
|
73
|
+
case 'done':
|
|
74
|
+
return { ...job, type: 'done' };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function pullStatus(pull) {
|
|
78
|
+
if (!pull.finished)
|
|
79
|
+
return 'running';
|
|
80
|
+
return pull.error ? 'error' : 'done';
|
|
81
|
+
}
|
|
82
|
+
function generationError(err) {
|
|
83
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
84
|
+
if (err instanceof ImageGenerationCancelledError)
|
|
85
|
+
return { code: 'cancelled', message };
|
|
86
|
+
if (err instanceof ImageCapacityError)
|
|
87
|
+
return { code: err.code, message };
|
|
88
|
+
if (err instanceof NoLocalImageModelError)
|
|
89
|
+
return { code: err.code, message };
|
|
90
|
+
const code = err?.code;
|
|
91
|
+
if (code === 'capacity-denied')
|
|
92
|
+
return { code: 'capacity_denied', message };
|
|
93
|
+
return { code: 'image_generation_failed', message };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* `/v1/images/*` — local image generation for apps.
|
|
97
|
+
*
|
|
98
|
+
* GET /models Installed and downloadable image models,
|
|
99
|
+
* with this machine's fit. Never part of
|
|
100
|
+
* `/v1/models`, so chat pickers never
|
|
101
|
+
* offer them.
|
|
102
|
+
* POST /models/ensure `{model: "sd-cpp:<id>"}` → ready, or a
|
|
103
|
+
* background download (`job_id`).
|
|
104
|
+
* GET /models/ensure/:jobId Snapshot of a download.
|
|
105
|
+
* GET /models/ensure/:jobId/events
|
|
106
|
+
* SSE of `AppEnsureEvent`s, closing after
|
|
107
|
+
* `done` or `error`. Disconnecting does
|
|
108
|
+
* not cancel the download.
|
|
109
|
+
* DELETE /models/ensure/:jobId Cancel the download; a later ensure
|
|
110
|
+
* resumes it.
|
|
111
|
+
* DELETE /models/:id Remove an installed model. A shared
|
|
112
|
+
* model belongs to another store: 409.
|
|
113
|
+
* POST /generations SSE of `AppImageGenerationEvent`s:
|
|
114
|
+
* progress, then one `done` (the PNG) or
|
|
115
|
+
* `error`. Closing the stream cancels the
|
|
116
|
+
* render, which stops the engine.
|
|
117
|
+
*/
|
|
118
|
+
export function v1ImagesRoutes(ctx) {
|
|
119
|
+
const app = new Hono();
|
|
120
|
+
const fitContext = { catalog: ctx.catalog, readConfig: () => ctx.store.readConfig() };
|
|
121
|
+
app.get('/models', async (c) => {
|
|
122
|
+
const provider = await ctx.images.current();
|
|
123
|
+
const [installed, items, fits] = await Promise.all([
|
|
124
|
+
provider.listInstalledModels(),
|
|
125
|
+
ctx.catalog.list('image-model'),
|
|
126
|
+
imageCatalogFits(fitContext).catch((error) => {
|
|
127
|
+
log.warn(`[v1-images] fit assessment failed: ${String(error)}`);
|
|
128
|
+
return null;
|
|
129
|
+
}),
|
|
130
|
+
]);
|
|
131
|
+
const storageRoots = modelStorageRoots({ home: ctx.home, engine: ENGINE });
|
|
132
|
+
const installedIds = new Set(installed.map((m) => m.id));
|
|
133
|
+
const fitById = new Map(fits?.models.map((m) => [m.id, m]) ?? []);
|
|
134
|
+
const downloading = new Set(ctx.images.pulls
|
|
135
|
+
.list()
|
|
136
|
+
.filter((p) => !p.finished)
|
|
137
|
+
.map((p) => p.id));
|
|
138
|
+
const data = [];
|
|
139
|
+
const seen = new Set();
|
|
140
|
+
for (const item of items) {
|
|
141
|
+
const manifest = item.manifest;
|
|
142
|
+
if (manifest.kind !== 'image-model')
|
|
143
|
+
continue;
|
|
144
|
+
seen.add(manifest.id);
|
|
145
|
+
const fit = fitById.get(manifest.id);
|
|
146
|
+
const isInstalled = installedIds.has(manifest.id);
|
|
147
|
+
data.push({
|
|
148
|
+
id: qualified(manifest.id),
|
|
149
|
+
object: 'image_model',
|
|
150
|
+
...describeManifest(manifest),
|
|
151
|
+
name: manifest.name,
|
|
152
|
+
availability: isInstalled
|
|
153
|
+
? 'available'
|
|
154
|
+
: downloading.has(manifest.id)
|
|
155
|
+
? 'downloading'
|
|
156
|
+
: 'download-required',
|
|
157
|
+
...(isInstalled && (await modelExistsOnlyReadOnly(storageRoots, manifest.id))
|
|
158
|
+
? { shared: true }
|
|
159
|
+
: {}),
|
|
160
|
+
...(fit ? { fit: { verdict: fit.verdict, reason: fit.reason } } : {}),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
// Installed models the catalog no longer lists still render.
|
|
164
|
+
for (const model of installed) {
|
|
165
|
+
if (seen.has(model.id))
|
|
166
|
+
continue;
|
|
167
|
+
data.push({
|
|
168
|
+
id: qualified(model.id),
|
|
169
|
+
object: 'image_model',
|
|
170
|
+
name: model.name,
|
|
171
|
+
availability: 'available',
|
|
172
|
+
...((await modelExistsOnlyReadOnly(storageRoots, model.id)) ? { shared: true } : {}),
|
|
173
|
+
...(model.supportsImg2Img !== undefined ? { supports_img2img: model.supportsImg2Img } : {}),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return c.json({ object: 'list', data });
|
|
177
|
+
});
|
|
178
|
+
app.post('/models/ensure', async (c) => {
|
|
179
|
+
const { model } = EnsureRequestSchema.parse(await c.req.json());
|
|
180
|
+
const id = catalogIdOf(model);
|
|
181
|
+
if (!id) {
|
|
182
|
+
return c.json(apiError('unsupported_backend', `Image models are ${ENGINE}:<id>`), 400);
|
|
183
|
+
}
|
|
184
|
+
const provider = await ctx.images.current();
|
|
185
|
+
const installed = await provider.listInstalledModels();
|
|
186
|
+
if (installed.some((m) => m.id === id)) {
|
|
187
|
+
return c.json({ status: 'ready', model_id: qualified(id) });
|
|
188
|
+
}
|
|
189
|
+
const refusal = await wontRunRefusal(() => imageModelFit(fitContext, id));
|
|
190
|
+
if (refusal)
|
|
191
|
+
return c.json(apiError(refusal.code, refusal.error), 409);
|
|
192
|
+
try {
|
|
193
|
+
await ctx.images.pulls.start(id);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
if (error.name === 'UnknownImageModelError') {
|
|
197
|
+
return c.json(apiError('unknown_model', `No image model ${model} in the catalog`), 404);
|
|
198
|
+
}
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
return c.json({ status: 'downloading', model_id: qualified(id), job_id: id }, 202);
|
|
202
|
+
});
|
|
203
|
+
app.get('/models/ensure/:jobId', (c) => {
|
|
204
|
+
const pull = ctx.images.pulls.get(c.req.param('jobId'));
|
|
205
|
+
if (!pull)
|
|
206
|
+
return c.json(apiError('job_not_found', 'job not found'), 404);
|
|
207
|
+
return c.json({
|
|
208
|
+
job_id: pull.id,
|
|
209
|
+
model_id: qualified(pull.id),
|
|
210
|
+
status: pullStatus(pull),
|
|
211
|
+
started_at: Date.parse(pull.startedAt),
|
|
212
|
+
bytes_written: pull.bytesWritten,
|
|
213
|
+
total_bytes: pull.totalBytes,
|
|
214
|
+
...(pull.error ? { error: pull.error } : {}),
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
app.get('/models/ensure/:jobId/events', (c) => {
|
|
218
|
+
const jobId = c.req.param('jobId');
|
|
219
|
+
const snapshot = ctx.images.pulls.get(jobId);
|
|
220
|
+
if (!snapshot)
|
|
221
|
+
return c.json(apiError('job_not_found', 'job not found'), 404);
|
|
222
|
+
return streamSSE(c, async (stream) => {
|
|
223
|
+
let closed = false;
|
|
224
|
+
let sawError = false;
|
|
225
|
+
const unsubscribe = ctx.images.pulls.subscribe(jobId, (event) => {
|
|
226
|
+
// A failed pull ends with an error and then its done; the error is terminal.
|
|
227
|
+
if (sawError)
|
|
228
|
+
return;
|
|
229
|
+
if (event.type === 'error')
|
|
230
|
+
sawError = true;
|
|
231
|
+
const terminal = event.type === 'done' || event.type === 'error';
|
|
232
|
+
void stream
|
|
233
|
+
.writeSSE({ data: JSON.stringify(ensureEvent(jobId, event, snapshot.totalBytes)) })
|
|
234
|
+
.then(() => {
|
|
235
|
+
if (terminal)
|
|
236
|
+
closed = true;
|
|
237
|
+
})
|
|
238
|
+
.catch(() => {
|
|
239
|
+
closed = true;
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
if (!unsubscribe)
|
|
243
|
+
return;
|
|
244
|
+
stream.onAbort(() => {
|
|
245
|
+
closed = true;
|
|
246
|
+
unsubscribe();
|
|
247
|
+
});
|
|
248
|
+
while (!closed) {
|
|
249
|
+
await stream.sleep(1000);
|
|
250
|
+
try {
|
|
251
|
+
await stream.writeSSE({ event: 'ping', data: '' });
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
closed = true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
unsubscribe();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
app.delete('/models/ensure/:jobId', (c) => {
|
|
261
|
+
if (!ctx.images.pulls.cancel(c.req.param('jobId'))) {
|
|
262
|
+
return c.json(apiError('job_not_found', 'No download in progress for that job'), 404);
|
|
263
|
+
}
|
|
264
|
+
return c.json({ cancelled: true });
|
|
265
|
+
});
|
|
266
|
+
app.delete('/models/:id', async (c) => {
|
|
267
|
+
const id = catalogIdOf(decodeURIComponent(c.req.param('id')));
|
|
268
|
+
if (!id)
|
|
269
|
+
return c.json(apiError('unsupported_backend', `Image models are ${ENGINE}:<id>`), 400);
|
|
270
|
+
const storageRoots = modelStorageRoots({ home: ctx.home, engine: ENGINE });
|
|
271
|
+
if (await modelExistsOnlyReadOnly(storageRoots, id)) {
|
|
272
|
+
return c.json(apiError('shared_model', 'This model is shared from another store and is not removed here'), 409);
|
|
273
|
+
}
|
|
274
|
+
// The engine may hold the model's files open.
|
|
275
|
+
await ctx.images.reset();
|
|
276
|
+
try {
|
|
277
|
+
await (await ctx.images.current()).deleteModel(id);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
if (error instanceof ReadOnlyModelError) {
|
|
281
|
+
return c.json(apiError('shared_model', error.message), 409);
|
|
282
|
+
}
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
return c.body(null, 204);
|
|
286
|
+
});
|
|
287
|
+
app.post('/generations', async (c) => {
|
|
288
|
+
const request = AppImageGenerationRequestSchema.parse(await c.req.json());
|
|
289
|
+
if (!ctx.images.available()) {
|
|
290
|
+
return c.json(apiError('images_unavailable', 'Image generation is not available on this computer'), 503);
|
|
291
|
+
}
|
|
292
|
+
const provider = await ctx.images.current();
|
|
293
|
+
const installed = await provider.listInstalledModels();
|
|
294
|
+
const requested = request.model ? catalogIdOf(request.model) : undefined;
|
|
295
|
+
if (request.model && !requested) {
|
|
296
|
+
return c.json(apiError('unsupported_backend', `Image models are ${ENGINE}:<id>`), 400);
|
|
297
|
+
}
|
|
298
|
+
const modelId = requested ?? (await provider.resolveDefaultModelId?.());
|
|
299
|
+
if (!modelId || !installed.some((m) => m.id === modelId)) {
|
|
300
|
+
const error = requested
|
|
301
|
+
? apiError('model_not_installed', `${request.model} is not installed; ensure it first`)
|
|
302
|
+
: apiError('no_local_image_model', new NoLocalImageModelError().message);
|
|
303
|
+
return c.json(error, 409);
|
|
304
|
+
}
|
|
305
|
+
const item = await ctx.catalog.get('image-model', modelId).catch(() => null);
|
|
306
|
+
const manifest = item?.manifest.kind === 'image-model' ? item.manifest : undefined;
|
|
307
|
+
const { width, height } = resolveImageSize({
|
|
308
|
+
...(request.aspect_ratio ? { aspectRatio: request.aspect_ratio } : {}),
|
|
309
|
+
...(request.quality ? { quality: request.quality } : {}),
|
|
310
|
+
}, {
|
|
311
|
+
defaultSidePx: imageModelDefaultSide(modelId),
|
|
312
|
+
...(manifest?.widthRange ? { widthRange: manifest.widthRange } : {}),
|
|
313
|
+
...(manifest?.heightRange ? { heightRange: manifest.heightRange } : {}),
|
|
314
|
+
});
|
|
315
|
+
return streamSSE(c, async (stream) => {
|
|
316
|
+
const controller = new AbortController();
|
|
317
|
+
const cancel = () => controller.abort();
|
|
318
|
+
stream.onAbort(cancel);
|
|
319
|
+
c.req.raw.signal.addEventListener('abort', cancel, { once: true });
|
|
320
|
+
const send = (event) => stream.writeSSE({ data: JSON.stringify(event) }).catch(cancel);
|
|
321
|
+
let last = '';
|
|
322
|
+
const onProgress = (progress) => {
|
|
323
|
+
const event = {
|
|
324
|
+
type: 'progress',
|
|
325
|
+
phase: progress.phase ?? 'sampling',
|
|
326
|
+
step: progress.step,
|
|
327
|
+
total_steps: progress.totalSteps,
|
|
328
|
+
...(progress.secondsPerStep ? { seconds_per_step: progress.secondsPerStep } : {}),
|
|
329
|
+
};
|
|
330
|
+
const key = JSON.stringify(event);
|
|
331
|
+
if (key === last)
|
|
332
|
+
return;
|
|
333
|
+
last = key;
|
|
334
|
+
void send(event);
|
|
335
|
+
};
|
|
336
|
+
try {
|
|
337
|
+
const out = await provider.generate({
|
|
338
|
+
prompt: request.prompt,
|
|
339
|
+
...(request.negative_prompt ? { negativePrompt: request.negative_prompt } : {}),
|
|
340
|
+
model: modelId,
|
|
341
|
+
width,
|
|
342
|
+
height,
|
|
343
|
+
...(request.seed !== undefined ? { seed: request.seed } : {}),
|
|
344
|
+
signal: controller.signal,
|
|
345
|
+
onProgress,
|
|
346
|
+
});
|
|
347
|
+
await send({
|
|
348
|
+
type: 'done',
|
|
349
|
+
b64_png: out.png.toString('base64'),
|
|
350
|
+
width: out.meta.widthPx,
|
|
351
|
+
height: out.meta.heightPx,
|
|
352
|
+
seed: out.meta.seed,
|
|
353
|
+
model: qualified(out.meta.model),
|
|
354
|
+
steps: out.meta.steps,
|
|
355
|
+
duration_ms: out.meta.durationMs,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
if (!controller.signal.aborted) {
|
|
360
|
+
log.warn(`[v1-images] generation failed: ${String(error)}`);
|
|
361
|
+
}
|
|
362
|
+
await send({ type: 'error', error: generationError(error) });
|
|
363
|
+
}
|
|
364
|
+
finally {
|
|
365
|
+
c.req.raw.signal.removeEventListener('abort', cancel);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
return app;
|
|
370
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createLogger } from '@bendyline/gezel';
|
|
1
|
+
import { createLogger, toolCallBlockedHistoryEntry } from '@bendyline/gezel';
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
3
|
import { bodyLimit } from 'hono/body-limit';
|
|
4
4
|
import { streamSSE } from 'hono/streaming';
|
|
@@ -11,6 +11,7 @@ import { resolveChatTarget, resolvePinnedModelTarget, } from '../openai-compat/c
|
|
|
11
11
|
import { engineFailure } from '../openai-compat/engine-failure.js';
|
|
12
12
|
import { runResponsesNonStreaming, runResponsesStreaming, } from '../openai-compat/responses-streaming.js';
|
|
13
13
|
import { ResponsesRequestSchema, translateResponsesRequest, } from '../openai-compat/responses-translate.js';
|
|
14
|
+
import { createToolCallScreen } from '../openai-compat/tool-call-screen.js';
|
|
14
15
|
import { flattenTranscriptIntoPrompt, parseGezelModelRef, resolveModelTarget, } from '../openai-compat/translate.js';
|
|
15
16
|
const log = createLogger('v1-responses');
|
|
16
17
|
const MAX_RESPONSES_BODY_BYTES = 16 * 1024 * 1024;
|
|
@@ -180,8 +181,19 @@ export function v1ResponsesRoutes(ctx, opts = {}) {
|
|
|
180
181
|
const detachRequestAbort = () => {
|
|
181
182
|
requestSignal.removeEventListener('abort', abortTurn);
|
|
182
183
|
};
|
|
184
|
+
const toolCallScreen = createToolCallScreen((tool, indicators) => {
|
|
185
|
+
const entry = toolCallBlockedHistoryEntry({
|
|
186
|
+
tool,
|
|
187
|
+
surface: 'responses',
|
|
188
|
+
indicators,
|
|
189
|
+
appId,
|
|
190
|
+
});
|
|
191
|
+
log.warn(`${entry.summary} (app ${appId})`);
|
|
192
|
+
void ctx.history?.log(entry).catch(() => { });
|
|
193
|
+
});
|
|
183
194
|
const runnerOptions = {
|
|
184
195
|
...(attachments.length > 0 ? { attachments } : {}),
|
|
196
|
+
toolCallScreen,
|
|
185
197
|
...(lengthCapTokens !== undefined ? { lengthCapTokens } : {}),
|
|
186
198
|
toolBindings: translated.toolBindings,
|
|
187
199
|
toolKinds: translated.toolKinds,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type LogOutput, type ProviderName } from '@bendyline/gezel';
|
|
2
|
+
import type { ImageProvider } from './providers/image/types.js';
|
|
2
3
|
import type { LLMProvider } from './providers/types.js';
|
|
3
4
|
import './http/auth-context.js';
|
|
4
5
|
/** Same shape the full service reports, so one handler serves either host. */
|
|
@@ -14,6 +15,8 @@ export interface InferenceServiceOptions {
|
|
|
14
15
|
home?: string;
|
|
15
16
|
/** Host-provided local providers, useful for testing without downloading weights. */
|
|
16
17
|
providers?: ReadonlyMap<ProviderName, LLMProvider>;
|
|
18
|
+
/** Host-provided image provider in place of the local engine, for tests. */
|
|
19
|
+
imageProvider?: ImageProvider;
|
|
17
20
|
onUnexpectedHttpError?: (event: InferenceHttpErrorEvent) => void;
|
|
18
21
|
/**
|
|
19
22
|
* Where log records go while this host runs; restored on `stop()`. An app
|
|
@@ -14,6 +14,7 @@ import { enginesRoutes } from './http/routes/engines.js';
|
|
|
14
14
|
import { modelBundleRoutes } from './http/routes/model-bundles.js';
|
|
15
15
|
import { v1ChatRoutes } from './http/routes/v1-chat.js';
|
|
16
16
|
import { v1EmbeddingsRoutes } from './http/routes/v1-embeddings.js';
|
|
17
|
+
import { v1ImagesRoutes } from './http/routes/v1-images.js';
|
|
17
18
|
import { v1KnowledgeRoutes } from './http/routes/v1-knowledge.js';
|
|
18
19
|
import { v1ModelsEnsureRoutes } from './http/routes/v1-models-ensure.js';
|
|
19
20
|
import { v1ModelsRoutes } from './http/routes/v1-models.js';
|
|
@@ -23,6 +24,7 @@ import { KnowledgeManager } from './knowledge/manager.js';
|
|
|
23
24
|
import { createWorkerCatalogHost } from './knowledge/worker-host.js';
|
|
24
25
|
import { embeddingsHealth, openEmbeddings, shutdownEmbeddings } from './memory/embeddings.js';
|
|
25
26
|
import { AppleFoundationModelsProvider } from './providers/apple-foundation-models/provider.js';
|
|
27
|
+
import { LocalImageEngine } from './providers/image/local-engine.js';
|
|
26
28
|
import { EngineInference } from './providers/native/inference.js';
|
|
27
29
|
import { resolveModelSessionDefaults } from './providers/session-defaults.js';
|
|
28
30
|
import { MlxRuntimeStatusBus } from './python/mlx-runtime-status-bus.js';
|
|
@@ -157,7 +159,25 @@ export async function startService(options = {}) {
|
|
|
157
159
|
await knowledge.start();
|
|
158
160
|
const relevance = new RelevanceModelManager({ home, readConfig: () => store.readConfig() });
|
|
159
161
|
cleanup.push(() => relevanceScorer().dispose());
|
|
160
|
-
const
|
|
162
|
+
const images = new LocalImageEngine({
|
|
163
|
+
home,
|
|
164
|
+
catalog,
|
|
165
|
+
arbiter: components.gpuArbiter,
|
|
166
|
+
readConfig: () => store.readConfig(),
|
|
167
|
+
...(options.imageProvider ? { provider: options.imageProvider } : {}),
|
|
168
|
+
});
|
|
169
|
+
cleanup.push(() => images.shutdown());
|
|
170
|
+
const context = {
|
|
171
|
+
home,
|
|
172
|
+
store,
|
|
173
|
+
catalog,
|
|
174
|
+
chat,
|
|
175
|
+
knowledge,
|
|
176
|
+
relevance,
|
|
177
|
+
images,
|
|
178
|
+
imageProvider: images,
|
|
179
|
+
...components,
|
|
180
|
+
};
|
|
161
181
|
const clientToken = randomBytes(32).toString('base64url');
|
|
162
182
|
const tokenBytes = Buffer.from(clientToken);
|
|
163
183
|
const app = new Hono();
|
|
@@ -191,12 +211,13 @@ export async function startService(options = {}) {
|
|
|
191
211
|
nodeVersion: process.versions.node,
|
|
192
212
|
platform: process.platform,
|
|
193
213
|
embeddings: embeddingsHealth(),
|
|
214
|
+
images: { available: images.available() },
|
|
194
215
|
}));
|
|
195
216
|
app.use('*', async (c, next) => {
|
|
196
217
|
const candidate = Buffer.from(c.req.header('authorization')?.replace(/^Bearer /, '') ?? '');
|
|
197
218
|
if (candidate.length !== tokenBytes.length || !timingSafeEqual(candidate, tokenBytes))
|
|
198
219
|
return c.json({ error: 'Unauthorized' }, 401);
|
|
199
|
-
c.set('auth', { appId: 'host', scopes: ['openai', 'knowledge'] });
|
|
220
|
+
c.set('auth', { appId: 'host', scopes: ['openai', 'knowledge', 'images'] });
|
|
200
221
|
return next();
|
|
201
222
|
});
|
|
202
223
|
const allowedProviders = ['llama-cpp', 'mlx', 'ds4', 'apple-foundation-models'];
|
|
@@ -207,6 +228,7 @@ export async function startService(options = {}) {
|
|
|
207
228
|
app.route('/v1/models/ensure', v1ModelsEnsureRoutes(context));
|
|
208
229
|
app.route('/v1/models', v1ModelsRoutes(context, { localOnly: true }));
|
|
209
230
|
app.route('/v1/knowledge', v1KnowledgeRoutes(context));
|
|
231
|
+
app.route('/v1/images', v1ImagesRoutes(context));
|
|
210
232
|
app.route('/api/engines', enginesRoutes(context));
|
|
211
233
|
app.route('/api/model-bundles', modelBundleRoutes(context));
|
|
212
234
|
for (const [engine, manager] of [
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"source": {
|
|
3
|
-
"url": "https://
|
|
4
|
-
"
|
|
5
|
-
|
|
3
|
+
"url": "https://huggingface.co/csukuangfj/kokoro-int8-multi-lang-v1_0/resolve/57adb37fc0e8d4bd430cfc1b8f8c2294945b9af2/",
|
|
4
|
+
"files": {
|
|
5
|
+
"lexicon-us-en.txt": "7daaab53a181be9885b853a8582bf1838186317e5dadacbcef9c426d6fa0da14",
|
|
6
|
+
"lexicon-gb-en.txt": "c4cbb37316f62210dff52718a7afcaae24f50c032cc75ab47ae67b831d1049e7",
|
|
7
|
+
"LICENSE": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"
|
|
8
|
+
}
|
|
6
9
|
},
|
|
7
10
|
"license": {
|
|
8
11
|
"file": "LICENSE",
|
|
@@ -0,0 +1,83 @@
|
|
|
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 { type GezelConfig, type ImageModelManifest, type ImageQuality, type MediaCapacity, type MediaFitAssessment, type MediaMemoryPool, type MediaMemoryRequirement, type MediaModelFit, type MediaModelFitResponse, type MediaWeightFile } from '@bendyline/gezel';
|
|
15
|
+
import type { CatalogService } from '@bendyline/gezel-catalog';
|
|
16
|
+
import type { CapacityBudgetKind } from '../providers/native/capacity-budget.js';
|
|
17
|
+
export interface MediaFitContext {
|
|
18
|
+
catalog: CatalogService;
|
|
19
|
+
readConfig: () => Promise<Pick<GezelConfig, 'localEngineMemoryGb'>>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The 409 body a pull route answers with when the model cannot run here, or
|
|
23
|
+
* null to let the pull start. A failure to assess never blocks a download:
|
|
24
|
+
* the launch path still applies the same check.
|
|
25
|
+
*/
|
|
26
|
+
export declare function wontRunRefusal(assess: () => Promise<MediaFitAssessment | null>): Promise<{
|
|
27
|
+
error: string;
|
|
28
|
+
code: string;
|
|
29
|
+
} | null>;
|
|
30
|
+
/**
|
|
31
|
+
* Where sd-server keeps weights: Metal on Apple Silicon, CPU on Intel Macs,
|
|
32
|
+
* CUDA or Vulkan elsewhere, falling back to the processor when no GPU
|
|
33
|
+
* answers. Windows on Arm runs on the processor unless discovery picked its
|
|
34
|
+
* CUDA build, which only an RTX Spark gets.
|
|
35
|
+
*/
|
|
36
|
+
export declare function imageMediaPool(kind: CapacityBudgetKind, host?: {
|
|
37
|
+
platform: NodeJS.Platform;
|
|
38
|
+
arch: string;
|
|
39
|
+
sdServerBin?: string | undefined;
|
|
40
|
+
}): MediaMemoryPool;
|
|
41
|
+
/** The ceilings the device ledger applies on this machine, automatic and under the person's limit. */
|
|
42
|
+
export declare function mediaCapacity(pool: MediaMemoryPool, readConfig: MediaFitContext['readConfig']): Promise<MediaCapacity>;
|
|
43
|
+
/** The memory sd-server needs for one model, from its weight files and its launch profile. */
|
|
44
|
+
export declare function sdServerRequirement(input: {
|
|
45
|
+
modelId: string;
|
|
46
|
+
weightsKind: 'checkpoint' | 'diffusion-model';
|
|
47
|
+
files: readonly MediaWeightFile[];
|
|
48
|
+
pool: MediaMemoryPool;
|
|
49
|
+
/** The render size to price; the model's default size when absent. */
|
|
50
|
+
megapixels?: number;
|
|
51
|
+
}): MediaMemoryRequirement;
|
|
52
|
+
/**
|
|
53
|
+
* What a launched sd-server claims on the device ledger.
|
|
54
|
+
*
|
|
55
|
+
* On a discrete card the fit figures charge graphics memory for weights only,
|
|
56
|
+
* because sd-server can stream components from RAM, and the download page
|
|
57
|
+
* must not call a model that offloads "won't run". A running render holds its
|
|
58
|
+
* compute buffers on the card too, though: FLUX.2 Klein at 1024px held
|
|
59
|
+
* 9.4 GiB of a 12 GiB card's dedicated memory against 4.9 GiB of weights
|
|
60
|
+
* (RTX 5070 Ti Laptop, Vulkan, M0 spike 2026-10-25). Claiming weights only
|
|
61
|
+
* let a resident chat model keep its memory, and the render then failed at
|
|
62
|
+
* its first step. The claim is the estimated peak, capped at the card so a
|
|
63
|
+
* model that must offload is admitted once the card is otherwise free.
|
|
64
|
+
*/
|
|
65
|
+
export declare function sdServerLedgerClaim(requirement: MediaMemoryRequirement, cardBytes: number): {
|
|
66
|
+
bytes: number;
|
|
67
|
+
gpuBytes: number;
|
|
68
|
+
};
|
|
69
|
+
/** Megapixels a model renders at a quality tier, inside its range. */
|
|
70
|
+
export declare function imageRenderMegapixels(manifest: Pick<ImageModelManifest, 'id' | 'widthRange' | 'heightRange'>, quality: ImageQuality): number;
|
|
71
|
+
export interface FitPlan {
|
|
72
|
+
pool: MediaMemoryPool;
|
|
73
|
+
capacity: MediaCapacity;
|
|
74
|
+
}
|
|
75
|
+
export declare function mediaFitResponse(plan: FitPlan, models: MediaModelFit[]): MediaModelFitResponse;
|
|
76
|
+
/**
|
|
77
|
+
* Every catalog image model's fit. With `quality`, each is priced at the size
|
|
78
|
+
* it renders at that tier rather than its default, which is how the direct
|
|
79
|
+
* image gezel's settings know whether High will fit.
|
|
80
|
+
*/
|
|
81
|
+
export declare function imageCatalogFits(ctx: MediaFitContext, quality?: ImageQuality): Promise<MediaModelFitResponse>;
|
|
82
|
+
/** One catalog image model's fit, or null when the id is not in the catalog. */
|
|
83
|
+
export declare function imageModelFit(ctx: MediaFitContext, id: string): Promise<MediaFitAssessment | null>;
|