@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
@@ -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;
@@ -1,4 +1,5 @@
1
1
  import type { ExternalToolCall, ImageAttachment, LLMSession, TurnUsage } from '../../providers/types.js';
2
+ import type { ToolCallScreen } from './tool-call-screen.js';
2
3
  export type ResponsesToolKind = 'function' | 'custom';
3
4
  /**
4
5
  * Maps a provider-safe local tool name back to the identity advertised on
@@ -243,6 +244,12 @@ export interface ResponsesSerializerOptions {
243
244
  toolNamespaces?: Record<string, string>;
244
245
  echo?: ResponsesRequestEcho;
245
246
  limits?: ResponsesSerializerLimits;
247
+ /**
248
+ * Withholds calls that name an egress indicator; see tool-call-screen.ts.
249
+ * A withheld call that was already announced closes `incomplete` with no
250
+ * arguments, since Responses clients run a call from its `done` item.
251
+ */
252
+ toolCallScreen?: ToolCallScreen;
246
253
  }
247
254
  /**
248
255
  * Stateful Responses output serializer. Every returned event is an immutable
@@ -259,6 +266,7 @@ export declare class ResponsesStreamSerializer {
259
266
  private readonly toolNamespaces;
260
267
  private readonly echo;
261
268
  private readonly limits;
269
+ private readonly toolCallScreen;
262
270
  private started;
263
271
  private terminal;
264
272
  private sequenceNumber;
@@ -305,6 +313,11 @@ export declare class ResponsesStreamSerializer {
305
313
  private finalizeActive;
306
314
  private finalizeMessage;
307
315
  private finalizeTool;
316
+ /**
317
+ * Close a screened call so no client can run it. One never announced stays
318
+ * out of the output; an announced one ends `incomplete` with no arguments.
319
+ */
320
+ private withholdTool;
308
321
  private reconcileToolCalls;
309
322
  private reconcileFinalText;
310
323
  private markActiveIncomplete;
@@ -64,6 +64,7 @@ export class ResponsesStreamSerializer {
64
64
  toolNamespaces;
65
65
  echo;
66
66
  limits;
67
+ toolCallScreen;
67
68
  started = false;
68
69
  terminal = false;
69
70
  sequenceNumber = 0;
@@ -88,6 +89,7 @@ export class ResponsesStreamSerializer {
88
89
  this.toolNamespaces = options.toolNamespaces ?? {};
89
90
  this.echo = options.echo ?? {};
90
91
  this.limits = { ...DEFAULT_LIMITS, ...options.limits };
92
+ this.toolCallScreen = options.toolCallScreen;
91
93
  }
92
94
  get isTerminal() {
93
95
  return this.terminal;
@@ -162,6 +164,7 @@ export class ResponsesStreamSerializer {
162
164
  const calls = this.echo.parallelToolCalls === false ? toolCalls.slice(0, 1) : toolCalls;
163
165
  events.push(...this.reconcileToolCalls(calls));
164
166
  events.push(...this.reconcileFinalText(text));
167
+ events.push(...this.pushTextDelta(this.toolCallScreen?.notice() ?? ''));
165
168
  events.push(...this.finalizeActive());
166
169
  this.terminal = true;
167
170
  this.completedAt = this.nowSeconds();
@@ -438,6 +441,8 @@ export class ResponsesStreamSerializer {
438
441
  const events = [];
439
442
  const finalRaw = finalArguments ?? state.rawArguments;
440
443
  this.assertToolCapacity(finalRaw.length);
444
+ if (this.toolCallScreen?.blocks(state.localName, finalRaw))
445
+ return this.withholdTool(state);
441
446
  if (!state.announced)
442
447
  events.push(...this.announceTool(state));
443
448
  if (!state.item || state.outputIndex === null)
@@ -503,6 +508,27 @@ export class ResponsesStreamSerializer {
503
508
  }));
504
509
  return events;
505
510
  }
511
+ /**
512
+ * Close a screened call so no client can run it. One never announced stays
513
+ * out of the output; an announced one ends `incomplete` with no arguments.
514
+ */
515
+ withholdTool(state) {
516
+ state.completed = true;
517
+ if (!state.announced || !state.item || state.outputIndex === null)
518
+ return [];
519
+ if (state.item.type === 'function_call')
520
+ state.item.arguments = '';
521
+ else
522
+ state.item.input = '';
523
+ state.item.status = 'incomplete';
524
+ return [
525
+ this.event({
526
+ type: 'response.output_item.done',
527
+ output_index: state.outputIndex,
528
+ item: cloneOutputItem(state.item),
529
+ }),
530
+ ];
531
+ }
506
532
  reconcileToolCalls(calls) {
507
533
  const events = [];
508
534
  const matched = new Set();
@@ -519,6 +545,9 @@ export class ResponsesStreamSerializer {
519
545
  }
520
546
  continue;
521
547
  }
548
+ // Never streamed, so a withheld call need not appear at all.
549
+ if (this.toolCallScreen?.blocks(call.name, call.arguments))
550
+ continue;
522
551
  if (this.active)
523
552
  events.push(...this.finalizeActive());
524
553
  events.push(...this.openTool(call.name, call.id));
@@ -626,6 +655,7 @@ function serializerOptions(model, nowSeconds, options) {
626
655
  ...(options.toolKinds ? { toolKinds: options.toolKinds } : {}),
627
656
  ...(options.toolNamespaces ? { toolNamespaces: options.toolNamespaces } : {}),
628
657
  ...(options.limits ? { limits: options.limits } : {}),
658
+ ...(options.toolCallScreen ? { toolCallScreen: options.toolCallScreen } : {}),
629
659
  echo: {
630
660
  ...options.echo,
631
661
  ...(options.echo?.maxOutputTokens === undefined && options.lengthCapTokens !== undefined
@@ -1,5 +1,6 @@
1
1
  import type { ExternalToolCall, ImageAttachment, LLMSession, SendAndWaitOpts } from '../../providers/types.js';
2
2
  import { type StreamingDiagnostics } from './streaming-telemetry.js';
3
+ import { type ToolCallScreen } from './tool-call-screen.js';
3
4
  /**
4
5
  * One-shot, non-streaming chat completion. Subscribes to usage to fill
5
6
  * the OpenAI `usage` envelope and returns the full response in the
@@ -52,6 +53,8 @@ export interface RunNonStreamingOpts {
52
53
  sendOptions?: Omit<SendAndWaitOpts, 'attachments'>;
53
54
  /** Effective output-token cap — reaching it reports `finish_reason: 'length'`. */
54
55
  lengthCapTokens?: number;
56
+ /** Withholds calls that name an egress indicator; see tool-call-screen.ts. */
57
+ toolCallScreen?: ToolCallScreen;
55
58
  }
56
59
  export declare function runNonStreaming(session: LLMSession, prompt: string, echoModel: string, nowSeconds: () => number, opts?: RunNonStreamingOpts): Promise<NonStreamingChatResult>;
57
60
  /**
@@ -117,6 +120,12 @@ export interface RunStreamingOpts {
117
120
  streamToolCallDeltas?: boolean;
118
121
  /** Tool names supplied by the caller; gates early textual-call detection. */
119
122
  toolCallNames?: readonly string[];
123
+ /**
124
+ * Withholds calls that name an egress indicator; see tool-call-screen.ts.
125
+ * With live tool-call deltas on, each call's closing tail is held back
126
+ * until the call is screened, so the client cannot run it early.
127
+ */
128
+ toolCallScreen?: ToolCallScreen;
120
129
  }
121
130
  export interface RunStreamingResult {
122
131
  content: string;
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { createChatProgressReporter } from './chat-progress.js';
3
3
  import { noteStreamingOutbound, noteStreamingProviderActivity, } from './streaming-telemetry.js';
4
+ import { screenableArgumentsPrefix } from './tool-call-screen.js';
4
5
  import { ToolCallStreamFilter } from './tool-call-stream-filter.js';
5
6
  /**
6
7
  * Truncation detection. An engine that reports its own stop reason
@@ -111,7 +112,7 @@ function streamedJsonObjectEnd(body, start) {
111
112
  return body.length;
112
113
  }
113
114
  export async function runNonStreaming(session, prompt, echoModel, nowSeconds, opts = {}) {
114
- const { attachments, sendOptions, lengthCapTokens } = opts;
115
+ const { attachments, sendOptions, lengthCapTokens, toolCallScreen } = opts;
115
116
  // Wrapped so TypeScript's control-flow narrowing doesn't infer
116
117
  // `usage` as `never` after the post-callback null check — closures
117
118
  // that assign to a captured variable confuse the analyzer.
@@ -127,8 +128,10 @@ export async function runNonStreaming(session, prompt, echoModel, nowSeconds, op
127
128
  ...(attachments && attachments.length > 0 ? { attachments } : {}),
128
129
  }
129
130
  : undefined;
130
- const content = await session.sendAndWait(prompt, sendOpts);
131
- const captured = session.capturedToolCalls?.() ?? [];
131
+ const reply = await session.sendAndWait(prompt, sendOpts);
132
+ const rawCaptured = session.capturedToolCalls?.() ?? [];
133
+ const captured = toolCallScreen ? toolCallScreen.admit(rawCaptured) : rawCaptured;
134
+ const content = `${reply}${toolCallScreen?.notice() ?? ''}`;
132
135
  const hasToolCalls = captured.length > 0;
133
136
  return {
134
137
  id: `chatcmpl-${randomUUID()}`,
@@ -173,7 +176,7 @@ export async function runNonStreaming(session, prompt, echoModel, nowSeconds, op
173
176
  * token).
174
177
  */
175
178
  export async function runStreaming(session, prompt, echoModel, sink, nowSeconds, opts = {}) {
176
- const { attachments, sendOptions, includeReasoning = false, keepaliveIntervalMs = 0, diagnostics, suppressTextualToolCalls = false, includeUsage = false, includeProgress = false, lengthCapTokens, onContentDelta, onReasoningDelta, onToolArgsDelta, streamToolCallDeltas = false, toolCallNames = [], } = opts;
179
+ const { attachments, sendOptions, includeReasoning = false, keepaliveIntervalMs = 0, diagnostics, suppressTextualToolCalls = false, includeUsage = false, includeProgress = false, lengthCapTokens, onContentDelta, onReasoningDelta, onToolArgsDelta, streamToolCallDeltas = false, toolCallNames = [], toolCallScreen, } = opts;
177
180
  const id = `chatcmpl-${randomUUID()}`;
178
181
  if (diagnostics)
179
182
  diagnostics.responseId = id;
@@ -198,6 +201,7 @@ export async function runStreaming(session, prompt, echoModel, sink, nowSeconds,
198
201
  const usageRef = { value: null };
199
202
  let visibleContent = '';
200
203
  let reasoningContent = '';
204
+ const sendableArguments = (received) => toolCallScreen ? screenableArgumentsPrefix(received) : received;
201
205
  const liveToolCalls = [];
202
206
  let activeLiveToolCall;
203
207
  let unnamedToolArguments = '';
@@ -238,7 +242,8 @@ export async function runStreaming(session, prompt, echoModel, sink, nowSeconds,
238
242
  index: meta.index ?? nextLiveToolIndex(),
239
243
  id: meta.id ?? `call_${randomUUID()}`,
240
244
  name,
241
- arguments: argumentsChunk,
245
+ arguments: sendableArguments(argumentsChunk),
246
+ received: argumentsChunk,
242
247
  };
243
248
  liveToolCalls.push(call);
244
249
  activeLiveToolCall = call;
@@ -269,10 +274,15 @@ export async function runStreaming(session, prompt, echoModel, sink, nowSeconds,
269
274
  }, 'tool_calls');
270
275
  return call;
271
276
  };
272
- const appendLiveToolArguments = (call, chunk) => {
277
+ const appendLiveToolArguments = (call, received) => {
278
+ if (!received)
279
+ return;
280
+ call.received += received;
281
+ const next = sendableArguments(call.received);
282
+ const chunk = next.slice(call.arguments.length);
273
283
  if (!chunk)
274
284
  return;
275
- call.arguments += chunk;
285
+ call.arguments = next;
276
286
  enqueueSSE({
277
287
  data: JSON.stringify({
278
288
  id,
@@ -516,9 +526,27 @@ export async function runStreaming(session, prompt, echoModel, sink, nowSeconds,
516
526
  }),
517
527
  }, 'content');
518
528
  }
519
- const captured = session.capturedToolCalls?.() ?? [];
529
+ const rawCaptured = session.capturedToolCalls?.() ?? [];
520
530
  if (diagnostics)
521
- diagnostics.capturedToolCalls = captured.length;
531
+ diagnostics.capturedToolCalls = rawCaptured.length;
532
+ // A withheld call's live stream stops short of its closing tail, so the
533
+ // client is left holding arguments it cannot parse.
534
+ const captured = toolCallScreen ? toolCallScreen.admit(rawCaptured) : rawCaptured;
535
+ const notice = toolCallScreen?.notice();
536
+ if (notice) {
537
+ visibleContent += notice;
538
+ onContentDelta?.(notice);
539
+ await writeSSE({
540
+ data: JSON.stringify({
541
+ id,
542
+ object: 'chat.completion.chunk',
543
+ created,
544
+ model: echoModel,
545
+ choices: [{ index: 0, delta: { content: notice }, finish_reason: null }],
546
+ ...nullUsage,
547
+ }),
548
+ }, 'content');
549
+ }
522
550
  const hasToolCalls = captured.length > 0;
523
551
  if (hasToolCalls) {
524
552
  if (!streamToolCallDeltas || liveToolCalls.length === 0) {
@@ -554,6 +582,17 @@ export async function runStreaming(session, prompt, echoModel, sink, nowSeconds,
554
582
  // any suffix that did not arrive live; never replay the complete
555
583
  // arguments, which would make clients concatenate invalid JSON.
556
584
  const matchedLive = new Set();
585
+ // Take the withheld calls' streams out of the running first, so an
586
+ // admitted call of the same tool name cannot be matched onto one.
587
+ for (const call of rawCaptured) {
588
+ if (captured.includes(call))
589
+ continue;
590
+ const live = liveToolCalls.find((candidate) => !matchedLive.has(candidate) &&
591
+ (candidate.id === call.id ||
592
+ (candidate.name === call.name && call.arguments.startsWith(candidate.arguments))));
593
+ if (live)
594
+ matchedLive.add(live);
595
+ }
557
596
  for (const call of captured) {
558
597
  const live = liveToolCalls.find((candidate) => !matchedLive.has(candidate) &&
559
598
  (candidate.id === call.id || candidate.name === call.name));
@@ -565,6 +604,7 @@ export async function runStreaming(session, prompt, echoModel, sink, nowSeconds,
565
604
  id: call.id,
566
605
  name: call.name,
567
606
  arguments: call.arguments,
607
+ received: call.arguments,
568
608
  });
569
609
  await writeSSE({
570
610
  data: JSON.stringify({
@@ -0,0 +1,28 @@
1
+ import { type EgressIndicator } from '@bendyline/gezel';
2
+ import type { ExternalToolCall } from '@bendyline/gezel/local-loop';
3
+ /**
4
+ * Screens the tool calls a `/v1` turn hands back to the calling app.
5
+ *
6
+ * On these surfaces the app runs the tools, often with a real shell (Codex,
7
+ * OpenCode, Pi on a local model), so the response is the last place Gezel
8
+ * sees a call before it runs. A call naming an egress indicator (core
9
+ * `security/egress-indicators.ts`) never reaches the app as a runnable call;
10
+ * the reply says so instead.
11
+ */
12
+ export interface ToolCallScreen {
13
+ /** The calls the app may run. Records the rest. */
14
+ admit(calls: readonly ExternalToolCall[]): ExternalToolCall[];
15
+ /** True when this call must not reach the app. Records it. */
16
+ blocks(name: string, args: string): boolean;
17
+ /** A line for the visible reply once anything was blocked. */
18
+ notice(): string | undefined;
19
+ }
20
+ export declare function createToolCallScreen(onBlocked?: (tool: string, indicators: EgressIndicator[]) => void): ToolCallScreen;
21
+ /**
22
+ * The part of a live-streamed argument string that is safe to send before
23
+ * the call has been screened: everything but a trailing run of closing
24
+ * brackets and whitespace. A tool call's arguments are a JSON object, which
25
+ * must end in `}`, so a client cannot parse — and so cannot run — a call
26
+ * until the screened tail arrives. Grows monotonically as `received` grows.
27
+ */
28
+ export declare function screenableArgumentsPrefix(received: string): string;
@@ -0,0 +1,43 @@
1
+ import { describeEgressIndicators, findEgressIndicators, findEgressIndicatorsInValue, } from '@bendyline/gezel';
2
+ export function createToolCallScreen(onBlocked) {
3
+ const blocked = [];
4
+ const seen = new Set();
5
+ const blocks = (name, args) => {
6
+ const indicators = indicatorsIn(args);
7
+ if (indicators.length === 0)
8
+ return false;
9
+ // The serializer and the final reconciliation can both see one call.
10
+ const key = `${name}\u0000${args}`;
11
+ if (!seen.has(key)) {
12
+ seen.add(key);
13
+ blocked.push(`${name} (it named ${describeEgressIndicators(indicators)})`);
14
+ onBlocked?.(name, indicators);
15
+ }
16
+ return true;
17
+ };
18
+ return {
19
+ admit: (calls) => calls.filter((call) => !blocks(call.name, call.arguments)),
20
+ blocks,
21
+ notice: () => blocked.length === 0
22
+ ? undefined
23
+ : `\n\n[Gezel stopped ${blocked.length === 1 ? 'a tool call' : `${blocked.length} tool calls`} before ${blocked.length === 1 ? 'it' : 'they'} ran: ${blocked.join('; ')}.]`,
24
+ };
25
+ }
26
+ function indicatorsIn(args) {
27
+ try {
28
+ return findEgressIndicatorsInValue(JSON.parse(args));
29
+ }
30
+ catch {
31
+ return findEgressIndicators(args);
32
+ }
33
+ }
34
+ /**
35
+ * The part of a live-streamed argument string that is safe to send before
36
+ * the call has been screened: everything but a trailing run of closing
37
+ * brackets and whitespace. A tool call's arguments are a JSON object, which
38
+ * must end in `}`, so a client cannot parse — and so cannot run — a call
39
+ * until the screened tail arrives. Grows monotonically as `received` grows.
40
+ */
41
+ export function screenableArgumentsPrefix(received) {
42
+ return received.replace(/[\s}\]]+$/, '');
43
+ }
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { createLogger, isLocalProvider } from '@bendyline/gezel';
2
+ import { createLogger, isLocalProvider, toolCallBlockedHistoryEntry, } from '@bendyline/gezel';
3
3
  import { Hono } from 'hono';
4
4
  import { streamSSE } from 'hono/streaming';
5
5
  import { ZodError } from 'zod';
@@ -14,6 +14,7 @@ import { engineFailure } from '../openai-compat/engine-failure.js';
14
14
  import { overlayOpenAiRequestTuning } from '../openai-compat/request-tuning.js';
15
15
  import { createStreamingDiagnostics, snapshotStreamingDiagnostics, } from '../openai-compat/streaming-telemetry.js';
16
16
  import { runNonStreaming, runStreaming } from '../openai-compat/streaming.js';
17
+ import { createToolCallScreen } from '../openai-compat/tool-call-screen.js';
17
18
  import { ChatCompletionRequestSchema, flattenTranscriptIntoPrompt, parseGezelModelRef, resolveModelTarget, translateMessagesWithPrefix, } from '../openai-compat/translate.js';
18
19
  const log = createLogger('v1-chat');
19
20
  // Match ChatManager's user-turn ceilings. Caller-owned app routes create a
@@ -402,6 +403,17 @@ export function v1ChatRoutes(ctx, opts = {}) {
402
403
  ctx.chat.recordExternalUsage?.(target.provider, u);
403
404
  });
404
405
  const appId = c.get('auth')?.appId ?? 'unauthenticated';
406
+ const toolCallScreen = createToolCallScreen((tool, indicators) => {
407
+ const entry = toolCallBlockedHistoryEntry({
408
+ tool,
409
+ surface: 'openai',
410
+ indicators,
411
+ appId,
412
+ ...(externalTurn ? { sessionId: externalTurn.sessionId } : {}),
413
+ });
414
+ log.warn(`${entry.summary} (app ${appId})`);
415
+ void ctx.history?.log(entry).catch(() => { });
416
+ });
405
417
  const logCompletion = () => {
406
418
  void ctx.history
407
419
  ?.log({
@@ -482,6 +494,7 @@ export function v1ChatRoutes(ctx, opts = {}) {
482
494
  ...(opts.keepaliveIntervalMs !== undefined
483
495
  ? { keepaliveIntervalMs: opts.keepaliveIntervalMs }
484
496
  : {}),
497
+ toolCallScreen,
485
498
  ...(opts.streamToolCallDeltas === true ? { streamToolCallDeltas: true } : {}),
486
499
  ...(opts.streamToolCallDeltas === true && externalTools
487
500
  ? { toolCallNames: externalTools.map((tool) => tool.name) }
@@ -574,6 +587,7 @@ export function v1ChatRoutes(ctx, opts = {}) {
574
587
  ...(attachments.length > 0 ? { attachments } : {}),
575
588
  sendOptions: { queue, timeoutMs: turnTimeoutMs },
576
589
  ...(lengthCapTokens !== undefined ? { lengthCapTokens } : {}),
590
+ toolCallScreen,
577
591
  });
578
592
  const choice = response.choices[0];
579
593
  await externalTurn
@@ -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;