@bendyline/gezel-inference 1.0.3 → 1.0.4

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 (95) 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/routes/v1-images.d.ts +44 -0
  57. package/dist/http/routes/v1-images.js +370 -0
  58. package/dist/inference-service.d.ts +3 -0
  59. package/dist/inference-service.js +24 -2
  60. package/dist/kokoro-lexicon/manifest.json +6 -3
  61. package/dist/models/image-fit.d.ts +83 -0
  62. package/dist/models/image-fit.js +163 -0
  63. package/dist/models/weight-facts.d.ts +36 -0
  64. package/dist/models/weight-facts.js +171 -0
  65. package/dist/providers/ds4/build-provider.js +28 -21
  66. package/dist/providers/ds4/dspark.d.ts +23 -5
  67. package/dist/providers/ds4/dspark.js +36 -8
  68. package/dist/providers/image/img2img-support.d.ts +40 -0
  69. package/dist/providers/image/img2img-support.js +74 -0
  70. package/dist/providers/image/local-engine.d.ts +31 -0
  71. package/dist/providers/image/local-engine.js +47 -0
  72. package/dist/providers/image/local-factory.d.ts +95 -0
  73. package/dist/providers/image/local-factory.js +270 -0
  74. package/dist/providers/image/mock.d.ts +23 -0
  75. package/dist/providers/image/mock.js +151 -0
  76. package/dist/providers/image/pull-registry.d.ts +106 -0
  77. package/dist/providers/image/pull-registry.js +227 -0
  78. package/dist/providers/image/sd-cpp.d.ts +200 -0
  79. package/dist/providers/image/sd-cpp.js +845 -0
  80. package/dist/providers/image/types.d.ts +235 -0
  81. package/dist/providers/image/types.js +20 -0
  82. package/dist/providers/llama-cpp/backend-health.d.ts +7 -0
  83. package/dist/providers/llama-cpp/backend-health.js +41 -1
  84. package/dist/providers/mlx/provider.js +13 -3
  85. package/dist/providers/native/supervisor.d.ts +8 -0
  86. package/dist/providers/native/supervisor.js +52 -4
  87. package/dist/providers/project-macro-loop-bail.js +5 -3
  88. package/dist/providers/provider-lifecycle.d.ts +21 -0
  89. package/dist/providers/provider-lifecycle.js +83 -0
  90. package/dist/providers/retirement-gate.d.ts +20 -0
  91. package/dist/providers/retirement-gate.js +55 -0
  92. package/dist/system/memory.js +6 -0
  93. package/dist/utils/image-meta.d.ts +12 -0
  94. package/dist/utils/image-meta.js +77 -0
  95. package/package.json +6 -6
@@ -40,12 +40,12 @@
40
40
  * but logs that it could not anchor to a baked-in digest.
41
41
  */
42
42
  /** Native release version this build pins. Placeholder until first public release. */
43
- export declare const NATIVE_ENGINE_RELEASE = "0.1.52";
43
+ export declare const NATIVE_ENGINE_RELEASE = "0.1.54";
44
44
  /** sha256 of the pinned release's `SHA256SUMS` asset. All-zeros = unpinned. */
45
- export declare const SHA256SUMS_DIGEST = "1a2183ad785671be0f78a0f25816f7c802ce668ebe4fa58d640d336ebcbd7023";
46
- /** Exact SHA256 values for every engine archive published by native-v0.1.52. */
45
+ export declare const SHA256SUMS_DIGEST = "087215a12096127a779b20e0dcb7d2ed9ce5dda1c81f33fdeb6b6eb730485b20";
46
+ /** Exact SHA256 values for every engine archive published by native-v0.1.54. */
47
47
  export declare const NATIVE_ENGINE_ARCHIVE_SHA256: Readonly<Record<string, string>>;
48
- /** Download size in bytes of every engine archive published by native-v0.1.52. */
48
+ /** Download size in bytes of every engine archive published by native-v0.1.54. */
49
49
  export declare const NATIVE_ENGINE_ARCHIVE_BYTES: Readonly<Record<string, number>>;
50
50
  /**
51
51
  * True only when the pinned release's standalone macOS archives were
@@ -40,47 +40,47 @@
40
40
  * but logs that it could not anchor to a baked-in digest.
41
41
  */
42
42
  /** Native release version this build pins. Placeholder until first public release. */
43
- export const NATIVE_ENGINE_RELEASE = '0.1.52';
43
+ export const NATIVE_ENGINE_RELEASE = '0.1.54';
44
44
  /** sha256 of the pinned release's `SHA256SUMS` asset. All-zeros = unpinned. */
45
- export const SHA256SUMS_DIGEST = '1a2183ad785671be0f78a0f25816f7c802ce668ebe4fa58d640d336ebcbd7023';
45
+ export const SHA256SUMS_DIGEST = '087215a12096127a779b20e0dcb7d2ed9ce5dda1c81f33fdeb6b6eb730485b20';
46
46
  // BEGIN PINNED NATIVE ARCHIVE HASHES
47
- /** Exact SHA256 values for every engine archive published by native-v0.1.52. */
47
+ /** Exact SHA256 values for every engine archive published by native-v0.1.54. */
48
48
  export const NATIVE_ENGINE_ARCHIVE_SHA256 = Object.freeze({
49
- 'gezel-native-0.1.52-darwin-arm64-metal.tar.gz': '8c8619530592692ad9fd905823a5bc7772a652d4257905919dac088c5fa86347',
50
- 'gezel-native-0.1.52-darwin-arm64.tar.gz': '3088b7d97968ea206bf58a516da2a9c635d7622672da220d48d790fd5555a797',
51
- 'gezel-native-0.1.52-linux-arm64-cpu.tar.gz': 'ec4b992eb372e735156691950eadb2aa8f7e5ef29437b53a9da6fe8cc2a7912b',
52
- 'gezel-native-0.1.52-linux-arm64-cuda.tar.gz': '4f97fd70f0d132f0bfcaadfd113f63add7d8b63982edf8b1b7a46301702047f5',
53
- 'gezel-native-0.1.52-linux-arm64.tar.gz': '3fa2c58ff75bea7ec0f739821250307bfba78cb026779ec3ad1ef935286af534',
54
- 'gezel-native-0.1.52-linux-x64-cpu.tar.gz': '1fbfd06acfea1ef9e2629c121da2d7da13ee2e76a0cebb8cf07b779938817966',
55
- 'gezel-native-0.1.52-linux-x64-cuda.tar.gz': '5c3c05f285c3922845b72e470ecfcd5e510b874a35ae841cd655a7098b541ffd',
56
- 'gezel-native-0.1.52-linux-x64-vulkan.tar.gz': '33a6dd016bd2cf0aada7f34c364a457acc524373b52a1d3f1f6e83c1240e93a7',
57
- 'gezel-native-0.1.52-linux-x64.tar.gz': 'f17afac30f984da17b6098bf5f9c5e88e9f9e128b8fbea0b5ff67a360a494313',
58
- 'gezel-native-0.1.52-win32-arm64-cpu.zip': '49f011b05eec55cc3f60730fdd46e3d28de331f60b195eb29abaa04fe62835a3',
59
- 'gezel-native-0.1.52-win32-arm64-cuda.zip': '7dbc99fd9516ea5a5cc49ad2c157bb66a240ecc2b5a5e22fae33142a6615caec',
60
- 'gezel-native-0.1.52-win32-arm64.zip': 'dfe61d86eae932e03d92665aa42b81121e21cfa76a85424d993b5726ee5fa504',
61
- 'gezel-native-0.1.52-win32-x64-cpu.zip': 'b1147052367daf694d6001ef0d45a8897a39a1d74cb776a42f22529d5c21f919',
62
- 'gezel-native-0.1.52-win32-x64-cuda.zip': '3c9568591d19ba04ceb12ae2e9f9f2a4ac7ade4597458a8674d444c06407324f',
63
- 'gezel-native-0.1.52-win32-x64-vulkan.zip': '9cb1e7721d1dbebfa830ec8cba1bbbbcb41f0145d60461e4bf50c4e347feecb2',
64
- 'gezel-native-0.1.52-win32-x64.zip': '7c94605b0783695174773ac2327274d91bb9d7ae31e7d932a2b9df78d44424df',
49
+ 'gezel-native-0.1.54-darwin-arm64-metal.tar.gz': 'f2990970303b954af2fd2e2bd31efef7d1c231bea16e026e0cdc0a7f9cc17842',
50
+ 'gezel-native-0.1.54-darwin-arm64.tar.gz': '850f28338c52f945996c55edd77cf14d451c9bc4a09f719f6608885ddab99687',
51
+ 'gezel-native-0.1.54-linux-arm64-cpu.tar.gz': '7dac7fed45c46b591cd6d597d639c385f54a3a367ac15d6097d3ad04d9c3c6e6',
52
+ 'gezel-native-0.1.54-linux-arm64-cuda.tar.gz': 'f73c4569c356c4584199513c6c1f1f0bd3f0151fb1845696e2fa43f2b3626803',
53
+ 'gezel-native-0.1.54-linux-arm64.tar.gz': 'f034ad2d3d12165c979d2fff2d34752c8d4f8ff836d3c416c9c64535f3bb652b',
54
+ 'gezel-native-0.1.54-linux-x64-cpu.tar.gz': '57de7d52b0e95e3886bf4ffad7e015b98ba8bbea96b95d8cb3e26bc1ff309e99',
55
+ 'gezel-native-0.1.54-linux-x64-cuda.tar.gz': 'c2d8616410f72b8bb217194f7c71d5175ed1c94ac6d9d94f660e0f535a3cff22',
56
+ 'gezel-native-0.1.54-linux-x64-vulkan.tar.gz': '89f3233f41861404db082219962eb25d1b2c3f6e42bbc2696fda122ce845371d',
57
+ 'gezel-native-0.1.54-linux-x64.tar.gz': 'f42e42d71187c6ef5af9685ebb65511bb7ac638815105f09f3df22cfdb9e6bce',
58
+ 'gezel-native-0.1.54-win32-arm64-cpu.zip': 'e482e2af92f28135aea2e21ae97290e231b0dc77eadc8cc532d2506304d27575',
59
+ 'gezel-native-0.1.54-win32-arm64-cuda.zip': 'ff6eaf7f070e9aba7fc087a155896f73b12664d867e0374af5546e941c153b82',
60
+ 'gezel-native-0.1.54-win32-arm64.zip': '9b854d824117fe56d7d532ab83c01693a97cf6b45752e69cb895aa183c8cff35',
61
+ 'gezel-native-0.1.54-win32-x64-cpu.zip': '93c8f22b47831ba8e0808033e87ce91cdb0d481890602857b6b8b5295cc7d50b',
62
+ 'gezel-native-0.1.54-win32-x64-cuda.zip': '84da0b362e502d0bb6e3509528e7a4ccff50c2b9fcaa2fa1a62b3d9b0ad9f5d2',
63
+ 'gezel-native-0.1.54-win32-x64-vulkan.zip': '1a3c99ce8343552fb17af80c36ba8ebb7c13ac8d6b4966f3aa3054018d41fa11',
64
+ 'gezel-native-0.1.54-win32-x64.zip': 'f224fcdea6945ce574d32bee6ace345f82847f317c40cfa9059feba9b87ac0ca',
65
65
  });
66
- /** Download size in bytes of every engine archive published by native-v0.1.52. */
66
+ /** Download size in bytes of every engine archive published by native-v0.1.54. */
67
67
  export const NATIVE_ENGINE_ARCHIVE_BYTES = Object.freeze({
68
- 'gezel-native-0.1.52-darwin-arm64-metal.tar.gz': 7056567,
69
- 'gezel-native-0.1.52-darwin-arm64.tar.gz': 42206061,
70
- 'gezel-native-0.1.52-linux-arm64-cpu.tar.gz': 8836310,
71
- 'gezel-native-0.1.52-linux-arm64-cuda.tar.gz': 620408821,
72
- 'gezel-native-0.1.52-linux-arm64.tar.gz': 42037958,
73
- 'gezel-native-0.1.52-linux-x64-cpu.tar.gz': 12914985,
74
- 'gezel-native-0.1.52-linux-x64-cuda.tar.gz': 993543364,
75
- 'gezel-native-0.1.52-linux-x64-vulkan.tar.gz': 25672744,
76
- 'gezel-native-0.1.52-linux-x64.tar.gz': 57183428,
77
- 'gezel-native-0.1.52-win32-arm64-cpu.zip': 5618616,
78
- 'gezel-native-0.1.52-win32-arm64-cuda.zip': 219555740,
79
- 'gezel-native-0.1.52-win32-arm64.zip': 42114295,
80
- 'gezel-native-0.1.52-win32-x64-cpu.zip': 8714442,
81
- 'gezel-native-0.1.52-win32-x64-cuda.zip': 749856660,
82
- 'gezel-native-0.1.52-win32-x64-vulkan.zip': 21123132,
83
- 'gezel-native-0.1.52-win32-x64.zip': 55662940,
68
+ 'gezel-native-0.1.54-darwin-arm64-metal.tar.gz': 7056387,
69
+ 'gezel-native-0.1.54-darwin-arm64.tar.gz': 42224129,
70
+ 'gezel-native-0.1.54-linux-arm64-cpu.tar.gz': 8838944,
71
+ 'gezel-native-0.1.54-linux-arm64-cuda.tar.gz': 620429935,
72
+ 'gezel-native-0.1.54-linux-arm64.tar.gz': 42037092,
73
+ 'gezel-native-0.1.54-linux-x64-cpu.tar.gz': 12914025,
74
+ 'gezel-native-0.1.54-linux-x64-cuda.tar.gz': 993587032,
75
+ 'gezel-native-0.1.54-linux-x64-vulkan.tar.gz': 25672135,
76
+ 'gezel-native-0.1.54-linux-x64.tar.gz': 57185943,
77
+ 'gezel-native-0.1.54-win32-arm64-cpu.zip': 5623741,
78
+ 'gezel-native-0.1.54-win32-arm64-cuda.zip': 242341587,
79
+ 'gezel-native-0.1.54-win32-arm64.zip': 42236457,
80
+ 'gezel-native-0.1.54-win32-x64-cpu.zip': 8719660,
81
+ 'gezel-native-0.1.54-win32-x64-cuda.zip': 749849595,
82
+ 'gezel-native-0.1.54-win32-x64-vulkan.zip': 21128362,
83
+ 'gezel-native-0.1.54-win32-x64.zip': 55668353,
84
84
  });
85
85
  // END PINNED NATIVE ARCHIVE HASHES
86
86
  /**
@@ -755,6 +755,15 @@ function stampEnv(engine, binPath, variant, versionDir) {
755
755
  process.env.GEZEL_NATIVE_BIN_DIR = versionDir;
756
756
  if (engine === 'llama-server' && variant)
757
757
  process.env.GEZEL_LLAMA_SERVER_BACKEND = variant;
758
+ // Windows on ARM's bare archive also carries sqlite-vec, which npm has no
759
+ // build of there. The sqlite driver reads this on every database open, so
760
+ // vector search starts with the next query rather than the next boot.
761
+ const platformKey = resolvePlatformKey();
762
+ const ext = process.platform === 'win32' ? 'dll' : process.platform === 'darwin' ? 'dylib' : 'so';
763
+ const sqliteVec = platformKey ? join(versionDir, platformKey, `vec0.${ext}`) : null;
764
+ if (sqliteVec && !process.env.GEZEL_SQLITE_VEC_PATH && existsSync(sqliteVec)) {
765
+ process.env.GEZEL_SQLITE_VEC_PATH = sqliteVec;
766
+ }
758
767
  }
759
768
  async function resolveGitHubToken() {
760
769
  if (process.env.GEZEL_GITHUB_TOKEN)
@@ -53,10 +53,15 @@ function internalCode(err) {
53
53
  const code = current.code;
54
54
  const wire = typeof code === 'string' ? WIRE_CODES.get(code) : undefined;
55
55
  // A crash the out-of-memory killer caused is a capacity fact, not a fault.
56
- if (wire === 'engine_crashed' &&
57
- current.panicKind === 'host-out-of-memory') {
56
+ const panicKind = current.panicKind;
57
+ if (wire === 'engine_crashed' && panicKind === 'host-out-of-memory') {
58
58
  return 'engine_out_of_memory';
59
59
  }
60
+ // A runtime the machine lacks: the engine never started, and retrying
61
+ // cannot help until it is installed.
62
+ if (wire === 'engine_crashed' && panicKind === 'missing-runtime') {
63
+ return 'engine_launch_failed';
64
+ }
60
65
  if (wire)
61
66
  return wire;
62
67
  current = current.cause;
@@ -0,0 +1,44 @@
1
+ import { type GezelConfig } from '@bendyline/gezel';
2
+ import type { CatalogService } from '@bendyline/gezel-catalog';
3
+ import { Hono } from 'hono';
4
+ import type { ImageModelPullRegistry } from '../../providers/image/pull-registry.js';
5
+ import { type ImageProvider } from '../../providers/image/types.js';
6
+ export interface ImagesRouteContext {
7
+ home: string;
8
+ catalog: CatalogService;
9
+ store: {
10
+ readConfig(): Promise<GezelConfig>;
11
+ };
12
+ images: {
13
+ /** Whether an engine can render here at all. */
14
+ available(): boolean;
15
+ current(): Promise<ImageProvider>;
16
+ /** Stop the engine, draining renders, before its model files change. */
17
+ reset(): Promise<void>;
18
+ pulls: ImageModelPullRegistry;
19
+ };
20
+ }
21
+ /**
22
+ * `/v1/images/*` — local image generation for apps.
23
+ *
24
+ * GET /models Installed and downloadable image models,
25
+ * with this machine's fit. Never part of
26
+ * `/v1/models`, so chat pickers never
27
+ * offer them.
28
+ * POST /models/ensure `{model: "sd-cpp:<id>"}` → ready, or a
29
+ * background download (`job_id`).
30
+ * GET /models/ensure/:jobId Snapshot of a download.
31
+ * GET /models/ensure/:jobId/events
32
+ * SSE of `AppEnsureEvent`s, closing after
33
+ * `done` or `error`. Disconnecting does
34
+ * not cancel the download.
35
+ * DELETE /models/ensure/:jobId Cancel the download; a later ensure
36
+ * resumes it.
37
+ * DELETE /models/:id Remove an installed model. A shared
38
+ * model belongs to another store: 409.
39
+ * POST /generations SSE of `AppImageGenerationEvent`s:
40
+ * progress, then one `done` (the PNG) or
41
+ * `error`. Closing the stream cancels the
42
+ * render, which stops the engine.
43
+ */
44
+ export declare function v1ImagesRoutes(ctx: ImagesRouteContext): Hono;
@@ -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,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 context = { home, store, catalog, chat, knowledge, relevance, ...components };
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://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/kokoro-int8-multi-lang-v1_0.tar.bz2",
4
- "sha256": "4c3052abaa60943a341f193888cf6abd68787dae6ab8ae5c925a706caa247e4e",
5
- "file": "kokoro-model.tar.bz2"
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",