@bendyline/gezel-inference 1.0.1 → 1.0.2

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 (70) hide show
  1. package/dist/engines/native-file-manifest.json +404 -404
  2. package/dist/engines/native-manifest.d.ts +4 -4
  3. package/dist/engines/native-manifest.js +34 -34
  4. package/dist/http/openai-compat/engine-failure.d.ts +25 -0
  5. package/dist/http/openai-compat/engine-failure.js +76 -0
  6. package/dist/http/openai-compat/responses-streaming.js +2 -1
  7. package/dist/http/openai-compat/responses-translate.d.ts +1 -1
  8. package/dist/http/openai-compat/translate.d.ts +1 -1
  9. package/dist/http/routes/engines.js +35 -0
  10. package/dist/http/routes/v1-chat.js +10 -1
  11. package/dist/http/routes/v1-models.js +6 -4
  12. package/dist/http/routes/v1-responses.js +7 -1
  13. package/dist/knowledge/catalog-host.d.ts +9 -1
  14. package/dist/knowledge/catalog-host.js +2 -0
  15. package/dist/knowledge/manager.d.ts +14 -0
  16. package/dist/knowledge/manager.js +53 -6
  17. package/dist/knowledge/search-worker.d.ts +1 -1
  18. package/dist/knowledge/spatial-query.d.ts +2 -3
  19. package/dist/knowledge/spatial-query.js +12 -6
  20. package/dist/knowledge/worker-host.js +2 -0
  21. package/dist/memory/embed-core.d.ts +16 -0
  22. package/dist/memory/embed-core.js +31 -0
  23. package/dist/memory/embed-worker.d.ts +7 -3
  24. package/dist/memory/embed-worker.js +13 -4
  25. package/dist/memory/embeddings.d.ts +17 -2
  26. package/dist/memory/embeddings.js +36 -7
  27. package/dist/models/ensure.d.ts +2 -0
  28. package/dist/models/ensure.js +9 -0
  29. package/dist/perf/responsiveness.d.ts +14 -1
  30. package/dist/perf/responsiveness.js +77 -7
  31. package/dist/providers/llama-cpp/backend-health.d.ts +30 -0
  32. package/dist/providers/llama-cpp/backend-health.js +64 -0
  33. package/dist/providers/llama-cpp/backend-quarantine.d.ts +40 -0
  34. package/dist/providers/llama-cpp/backend-quarantine.js +100 -0
  35. package/dist/providers/llama-cpp/build-provider.js +141 -144
  36. package/dist/providers/llama-cpp/cache-adapter.js +2 -1
  37. package/dist/providers/llama-cpp/devices.d.ts +11 -2
  38. package/dist/providers/llama-cpp/devices.js +10 -4
  39. package/dist/providers/llama-cpp/engine-flags.d.ts +0 -18
  40. package/dist/providers/llama-cpp/engine-flags.js +12 -18
  41. package/dist/providers/llama-cpp/gguf-metadata.js +20 -3
  42. package/dist/providers/llama-cpp/gpu-startup-watch.d.ts +41 -0
  43. package/dist/providers/llama-cpp/gpu-startup-watch.js +58 -0
  44. package/dist/providers/llama-cpp/models.d.ts +6 -0
  45. package/dist/providers/llama-cpp/models.js +19 -1
  46. package/dist/providers/llama-cpp/offload-planner.d.ts +3 -18
  47. package/dist/providers/llama-cpp/offload-planner.js +3 -14
  48. package/dist/providers/llama-cpp/provider.d.ts +8 -0
  49. package/dist/providers/llama-cpp/provider.js +4 -1
  50. package/dist/providers/mlx/provider.js +3 -4
  51. package/dist/providers/mlx/python/qwen4_ple.py +9 -2
  52. package/dist/providers/native/capacity-broker.d.ts +21 -15
  53. package/dist/providers/native/capacity-broker.js +16 -10
  54. package/dist/providers/native/capacity-budget.d.ts +35 -3
  55. package/dist/providers/native/capacity-budget.js +49 -5
  56. package/dist/providers/native/device-capacity.d.ts +1 -1
  57. package/dist/providers/native/device-capacity.js +23 -5
  58. package/dist/providers/native/local-engine-runtime.js +16 -19
  59. package/dist/providers/native/measured-budget.js +2 -0
  60. package/dist/providers/native/supervisor.d.ts +25 -0
  61. package/dist/providers/native/supervisor.js +59 -7
  62. package/dist/system/amdgpu-adapters.d.ts +18 -0
  63. package/dist/system/amdgpu-adapters.js +55 -0
  64. package/dist/system/gpu-carve-out.d.ts +84 -0
  65. package/dist/system/gpu-carve-out.js +111 -0
  66. package/dist/system/memory.d.ts +17 -1
  67. package/dist/system/memory.js +94 -13
  68. package/dist/transformers-cache.d.ts +2 -0
  69. package/dist/transformers-cache.js +6 -3
  70. package/package.json +5 -5
@@ -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.49";
43
+ export declare const NATIVE_ENGINE_RELEASE = "0.1.51";
44
44
  /** sha256 of the pinned release's `SHA256SUMS` asset. All-zeros = unpinned. */
45
- export declare const SHA256SUMS_DIGEST = "51e9d1fcd18174967fed451d11a7401035076330dfedadd6a0ce396ab636cff7";
46
- /** Exact SHA256 values for every engine archive published by native-v0.1.49. */
45
+ export declare const SHA256SUMS_DIGEST = "499a87aa18e113cdb52ee277bff71365e9176b63b536dbea7f3c2aa5ff5dbe68";
46
+ /** Exact SHA256 values for every engine archive published by native-v0.1.51. */
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.49. */
48
+ /** Download size in bytes of every engine archive published by native-v0.1.51. */
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,45 +40,45 @@
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.49';
43
+ export const NATIVE_ENGINE_RELEASE = '0.1.51';
44
44
  /** sha256 of the pinned release's `SHA256SUMS` asset. All-zeros = unpinned. */
45
- export const SHA256SUMS_DIGEST = '51e9d1fcd18174967fed451d11a7401035076330dfedadd6a0ce396ab636cff7';
45
+ export const SHA256SUMS_DIGEST = '499a87aa18e113cdb52ee277bff71365e9176b63b536dbea7f3c2aa5ff5dbe68';
46
46
  // BEGIN PINNED NATIVE ARCHIVE HASHES
47
- /** Exact SHA256 values for every engine archive published by native-v0.1.49. */
47
+ /** Exact SHA256 values for every engine archive published by native-v0.1.51. */
48
48
  export const NATIVE_ENGINE_ARCHIVE_SHA256 = Object.freeze({
49
- 'gezel-native-0.1.49-darwin-arm64-metal.tar.gz': 'a484487893151fdbe1ba5b3ea9d4dff3692efe8735e0a681d8f4ab84d5ffb09d',
50
- 'gezel-native-0.1.49-darwin-arm64.tar.gz': '7dc049ffd09dfb51befbf5c955e569556de6024991478eeb6900646e4672c723',
51
- 'gezel-native-0.1.49-linux-arm64-cpu.tar.gz': '19490ce26e843c2cab7321b91f9fac781ab3bab9ade6e3945543b312bf1a29e8',
52
- 'gezel-native-0.1.49-linux-arm64-cuda.tar.gz': '6802852803b0748d7ced99805a8aac281637f8816d02937460e64fbcd8da0637',
53
- 'gezel-native-0.1.49-linux-arm64.tar.gz': '8d3cd77b5c029fc0c4b4ae83000a72450008973eaf69168c4a21c58bfb75bf33',
54
- 'gezel-native-0.1.49-linux-x64-cpu.tar.gz': '030eaf798727ab13dcd7e7f4b3124c0f02e6f638e58f81c5d9a399c57d1c0a0a',
55
- 'gezel-native-0.1.49-linux-x64-cuda.tar.gz': 'd31d21d0c4c5504c9f29eaed32b500b26676bec0fd5ed23fdfd598baecafd748',
56
- 'gezel-native-0.1.49-linux-x64-vulkan.tar.gz': '114956cb94d174f00105f86b97ba932e1583cac80d9d0ab6b0edf396ea410506',
57
- 'gezel-native-0.1.49-linux-x64.tar.gz': 'd6a711c2be9329b00ad6bb20e4c16062677ef4d5804a2033d579b2347f5e2802',
58
- 'gezel-native-0.1.49-win32-arm64-cpu.zip': 'c2e8f10a45bae6cee4801845b605588c5b8b7b3c2e81a512e89296a2f9b7afce',
59
- 'gezel-native-0.1.49-win32-arm64.zip': '23173db21533dfe3f3189f5582cde9a0550859515b2c94688fb34d342d97e3e5',
60
- 'gezel-native-0.1.49-win32-x64-cpu.zip': 'cf6a8806e2c2b0a964804169cfb3d9283032536a6c262dc70b65a37b45451f3e',
61
- 'gezel-native-0.1.49-win32-x64-cuda.zip': '9a1e090829cf2f16389e1a01506853d8ac3ed151a32994831000c1c3658cef08',
62
- 'gezel-native-0.1.49-win32-x64-vulkan.zip': '0b7fe1cab3b881ac4a5cc24f08b0206c38edab30940ea79888a70776925a8bfb',
63
- 'gezel-native-0.1.49-win32-x64.zip': 'e8996f82ea358903010e050d6b8b93beec683286af512b11cd8eeefdea50aa6d',
49
+ 'gezel-native-0.1.51-darwin-arm64-metal.tar.gz': '5abbfd156fbcc717da96c89dc5baa507a56d2b55b501f1a975803a763e6414c4',
50
+ 'gezel-native-0.1.51-darwin-arm64.tar.gz': '799bca88b0ea10aeb2dad9ed3065c9396f42383a6fd5c7347ed76abf0706f128',
51
+ 'gezel-native-0.1.51-linux-arm64-cpu.tar.gz': '1612e5928693262884bcc2c60ba3975af4dc4267c2bd75597ef91e83681fa96e',
52
+ 'gezel-native-0.1.51-linux-arm64-cuda.tar.gz': '4a457df5dbb8475ca0eb2f1a2373f1960bbb3f86a0ecd938a407f884cd6026a2',
53
+ 'gezel-native-0.1.51-linux-arm64.tar.gz': '2aaead65bfd42ffef9a1f45a70fa24095bc2814842750536989cbcb7a9a0db24',
54
+ 'gezel-native-0.1.51-linux-x64-cpu.tar.gz': '7453d6a3ef2de7ece967a033d5bd634789e41edefa4a823b580329f0ad98abb0',
55
+ 'gezel-native-0.1.51-linux-x64-cuda.tar.gz': 'ac1054a33339bcf6167c309b867f5671def58e036bb612978c4a434cd7ac39d6',
56
+ 'gezel-native-0.1.51-linux-x64-vulkan.tar.gz': '6739c9dca6cbcf53fb5d69dbd342ac6be49fe2f0d86471d66975e61a64c95e9f',
57
+ 'gezel-native-0.1.51-linux-x64.tar.gz': 'a176ac570131db874e0a81fdec939415dd161a639827219a82f2a610f5ebd8d3',
58
+ 'gezel-native-0.1.51-win32-arm64-cpu.zip': 'f79c0c8490d46f2d60e73fd948bf74ae93dfc7c650a878f7db5e227bbdbb85b9',
59
+ 'gezel-native-0.1.51-win32-arm64.zip': 'c829c9c1fd08756aff9e677ea1addcf80a8739f13df3c9b6788a6237e53efacb',
60
+ 'gezel-native-0.1.51-win32-x64-cpu.zip': 'f8e0ffeacc8aeea1f2fc91024382bdcdaeeeb65b8a45935af422acb4d7027190',
61
+ 'gezel-native-0.1.51-win32-x64-cuda.zip': '6bd7dc2c43443a847058a4c4f1c53281c99563c214394a92470281a70f725d1e',
62
+ 'gezel-native-0.1.51-win32-x64-vulkan.zip': 'e20fb148c854f267aa50981525e337c38d3ee496fd483393a676251d9c0a496f',
63
+ 'gezel-native-0.1.51-win32-x64.zip': 'ca5d7866fa2ff2e89da9267fcb409eb9aff01ef1c43ed726327e4967d3ed5e56',
64
64
  });
65
- /** Download size in bytes of every engine archive published by native-v0.1.49. */
65
+ /** Download size in bytes of every engine archive published by native-v0.1.51. */
66
66
  export const NATIVE_ENGINE_ARCHIVE_BYTES = Object.freeze({
67
- 'gezel-native-0.1.49-darwin-arm64-metal.tar.gz': 6895181,
68
- 'gezel-native-0.1.49-darwin-arm64.tar.gz': 41543602,
69
- 'gezel-native-0.1.49-linux-arm64-cpu.tar.gz': 8681742,
70
- 'gezel-native-0.1.49-linux-arm64-cuda.tar.gz': 618839431,
71
- 'gezel-native-0.1.49-linux-arm64.tar.gz': 41977168,
72
- 'gezel-native-0.1.49-linux-x64-cpu.tar.gz': 13085558,
73
- 'gezel-native-0.1.49-linux-x64-cuda.tar.gz': 987396266,
74
- 'gezel-native-0.1.49-linux-x64-vulkan.tar.gz': 27140717,
75
- 'gezel-native-0.1.49-linux-x64.tar.gz': 58057802,
76
- 'gezel-native-0.1.49-win32-arm64-cpu.zip': 5459336,
77
- 'gezel-native-0.1.49-win32-arm64.zip': 42048477,
78
- 'gezel-native-0.1.49-win32-x64-cpu.zip': 8603082,
79
- 'gezel-native-0.1.49-win32-x64-cuda.zip': 641853362,
80
- 'gezel-native-0.1.49-win32-x64-vulkan.zip': 22326156,
81
- 'gezel-native-0.1.49-win32-x64.zip': 56551783,
67
+ 'gezel-native-0.1.51-darwin-arm64-metal.tar.gz': 7056609,
68
+ 'gezel-native-0.1.51-darwin-arm64.tar.gz': 41556782,
69
+ 'gezel-native-0.1.51-linux-arm64-cpu.tar.gz': 8836261,
70
+ 'gezel-native-0.1.51-linux-arm64-cuda.tar.gz': 619480627,
71
+ 'gezel-native-0.1.51-linux-arm64.tar.gz': 41979458,
72
+ 'gezel-native-0.1.51-linux-x64-cpu.tar.gz': 12914417,
73
+ 'gezel-native-0.1.51-linux-x64-cuda.tar.gz': 989771250,
74
+ 'gezel-native-0.1.51-linux-x64-vulkan.tar.gz': 25672179,
75
+ 'gezel-native-0.1.51-linux-x64.tar.gz': 58057226,
76
+ 'gezel-native-0.1.51-win32-arm64-cpu.zip': 5618599,
77
+ 'gezel-native-0.1.51-win32-arm64.zip': 42055408,
78
+ 'gezel-native-0.1.51-win32-x64-cpu.zip': 8714418,
79
+ 'gezel-native-0.1.51-win32-x64-cuda.zip': 749850276,
80
+ 'gezel-native-0.1.51-win32-x64-vulkan.zip': 21123169,
81
+ 'gezel-native-0.1.51-win32-x64.zip': 56558016,
82
82
  });
83
83
  // END PINNED NATIVE ARCHIVE HASHES
84
84
  /**
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The OpenAI-compatible routes' vocabulary for an on-device engine failure.
3
+ *
4
+ * Providers already throw typed errors that say exactly what went wrong: the
5
+ * capacity broker refused the model's memory, the engine crashed, the prompt
6
+ * overflowed its context. These routes kept only the message and stamped every
7
+ * one `provider_error`, so a connected app could not tell "this machine cannot
8
+ * fit the model" from "try again in a second" without parsing prose — and the
9
+ * prose is exactly what a sanitizing proxy strips. Each internal code maps to
10
+ * one stable wire code here; anything unrecognized stays `provider_error`.
11
+ */
12
+ export type EngineFailureCode = 'capacity_denied' | 'engine_busy' | 'engine_launch_failed' | 'engine_crashed' | 'engine_out_of_memory' | 'context_length_exceeded' | 'engine_not_ready' | 'model_not_ready';
13
+ export interface EngineFailure {
14
+ code: EngineFailureCode | 'provider_error';
15
+ type: 'server_error' | 'invalid_request_error';
16
+ status: 400 | 500;
17
+ }
18
+ /**
19
+ * Stand-ins for an engine failure's own message wherever a response body must
20
+ * not carry internal detail (the service's opaque-error middleware). The
21
+ * provider's message can name binaries and model paths.
22
+ */
23
+ export declare const ENGINE_FAILURE_SUMMARIES: Readonly<Record<EngineFailureCode, string>>;
24
+ export declare function isEngineFailureCode(value: unknown): value is EngineFailureCode;
25
+ export declare function engineFailure(err: unknown): EngineFailure;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * The OpenAI-compatible routes' vocabulary for an on-device engine failure.
3
+ *
4
+ * Providers already throw typed errors that say exactly what went wrong: the
5
+ * capacity broker refused the model's memory, the engine crashed, the prompt
6
+ * overflowed its context. These routes kept only the message and stamped every
7
+ * one `provider_error`, so a connected app could not tell "this machine cannot
8
+ * fit the model" from "try again in a second" without parsing prose — and the
9
+ * prose is exactly what a sanitizing proxy strips. Each internal code maps to
10
+ * one stable wire code here; anything unrecognized stays `provider_error`.
11
+ */
12
+ /** Internal error `code` (as thrown) → wire code (as sent). */
13
+ const WIRE_CODES = new Map([
14
+ ['capacity-denied', 'capacity_denied'],
15
+ ['engine-busy', 'engine_busy'],
16
+ ['engine-launch', 'engine_launch_failed'],
17
+ ['native-engine-crash', 'engine_crashed'],
18
+ ['context-overflow', 'context_length_exceeded'],
19
+ ['engine-not-ready', 'engine_not_ready'],
20
+ ['model-not-ready', 'model_not_ready'],
21
+ ]);
22
+ /**
23
+ * Stand-ins for an engine failure's own message wherever a response body must
24
+ * not carry internal detail (the service's opaque-error middleware). The
25
+ * provider's message can name binaries and model paths.
26
+ */
27
+ export const ENGINE_FAILURE_SUMMARIES = {
28
+ capacity_denied: 'This machine does not have enough free memory to run this model right now.',
29
+ engine_busy: 'The on-device engine is busy with another request. Try again shortly.',
30
+ engine_launch_failed: 'The on-device engine could not be started on this machine.',
31
+ engine_crashed: 'The on-device engine stopped unexpectedly. Try again.',
32
+ engine_out_of_memory: 'This computer ran out of memory and the on-device engine was stopped. Close other programs or use a smaller model, then try again.',
33
+ context_length_exceeded: 'The prompt is longer than this model can read at once.',
34
+ engine_not_ready: 'The on-device engine is still getting ready. Try again shortly.',
35
+ model_not_ready: 'This model is not ready yet.',
36
+ };
37
+ export function isEngineFailureCode(value) {
38
+ return typeof value === 'string' && Object.hasOwn(ENGINE_FAILURE_SUMMARIES, value);
39
+ }
40
+ const MAX_CAUSE_DEPTH = 5;
41
+ /**
42
+ * The outermost recognized code wins: a wrapper such as
43
+ * `NativeEngineCrashedError` is the provider's own classification of the
44
+ * cause it carries.
45
+ */
46
+ function internalCode(err) {
47
+ const seen = new Set();
48
+ let current = err;
49
+ for (let depth = 0; depth < MAX_CAUSE_DEPTH; depth++) {
50
+ if (!current || typeof current !== 'object' || seen.has(current))
51
+ return undefined;
52
+ seen.add(current);
53
+ const code = current.code;
54
+ const wire = typeof code === 'string' ? WIRE_CODES.get(code) : undefined;
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') {
58
+ return 'engine_out_of_memory';
59
+ }
60
+ if (wire)
61
+ return wire;
62
+ current = current.cause;
63
+ }
64
+ return undefined;
65
+ }
66
+ export function engineFailure(err) {
67
+ const code = internalCode(err);
68
+ if (!code)
69
+ return { code: 'provider_error', type: 'server_error', status: 500 };
70
+ // OpenAI's own spelling and status for an over-long prompt. A 500 makes
71
+ // every SDK retry a request that cannot fit no matter how often it is sent.
72
+ if (code === 'context_length_exceeded') {
73
+ return { code, type: 'invalid_request_error', status: 400 };
74
+ }
75
+ return { code, type: 'server_error', status: 500 };
76
+ }
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { engineFailure } from './engine-failure.js';
2
3
  import { unwrapCustomToolInput } from './responses-translate.js';
3
4
  const DEFAULT_LIMITS = {
4
5
  maxOutputTextChars: 4_000_000,
@@ -673,7 +674,7 @@ export async function runResponsesStreaming(session, prompt, echoModel, sink, no
673
674
  }
674
675
  catch (error) {
675
676
  options.onProviderError?.(error);
676
- enqueue(serializer.fail('The model provider failed while generating this response.', 'provider_error'));
677
+ enqueue(serializer.fail('The model provider failed while generating this response.', engineFailure(error).code));
677
678
  }
678
679
  finally {
679
680
  unsubscribeDelta();
@@ -134,8 +134,8 @@ export declare const ResponsesRequestSchema: z.ZodObject<{
134
134
  }, z.core.$loose>]>>>;
135
135
  tool_choice: z.ZodDefault<z.ZodUnion<readonly [z.ZodEnum<{
136
136
  none: "none";
137
- auto: "auto";
138
137
  required: "required";
138
+ auto: "auto";
139
139
  }>, z.ZodObject<{
140
140
  type: z.ZodEnum<{
141
141
  function: "function";
@@ -121,8 +121,8 @@ export declare const ChatCompletionRequestSchema: z.ZodObject<{
121
121
  }, z.core.$strip>>>;
122
122
  tool_choice: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
123
123
  none: "none";
124
- auto: "auto";
125
124
  required: "required";
125
+ auto: "auto";
126
126
  }>, z.ZodObject<{
127
127
  type: z.ZodLiteral<"function">;
128
128
  function: z.ZodObject<{
@@ -6,6 +6,8 @@ import { streamSSE } from 'hono/streaming';
6
6
  import { z } from 'zod';
7
7
  import { effectiveEngineRelease, isEnginePinned, pinnedEngineArchiveBytes, } from '../../engines/native-manifest.js';
8
8
  import { KNOWN_ENGINES, isKnownEngine } from '../../engines/registry.js';
9
+ import { llamaBackendHealthView } from '../../providers/llama-cpp/backend-health.js';
10
+ import { isLlamaBackend, liftLlamaQuarantine, llamaQuarantineView, } from '../../providers/llama-cpp/backend-quarantine.js';
9
11
  import { EngineBusyError } from '../../providers/native/capacity-broker.js';
10
12
  import { invalidateModelsCache } from '../model-inventory-cache.js';
11
13
  import { isMachineEngineProvider, machineEngineProxy } from './machine-engine-proxy.js';
@@ -212,6 +214,39 @@ export function enginesRoutes(ctx) {
212
214
  }
213
215
  return c.json({ policy: effective });
214
216
  });
217
+ /**
218
+ * llama-server builds switched off on this machine after crashing while
219
+ * starting, and the undo behind the engine's Settings tab. Engine-owner
220
+ * state like the policies above: the quarantine file lives in the home of
221
+ * the process that launches the engines, so packaged user daemons proxy
222
+ * this to the machine engine.
223
+ */
224
+ app.get('/llama-cpp/quarantine', (c) => c.json(llamaQuarantineView(ctx.home)));
225
+ /**
226
+ * Whether llama-server is getting this machine's hardware: a CUDA build the
227
+ * driver cannot run, a GPU build that started without its GPU, the
228
+ * out-of-memory killer. Engine-owner state, proxied like the quarantine.
229
+ */
230
+ app.get('/llama-cpp/backend-health', (c) => c.json(llamaBackendHealthView()));
231
+ app.delete('/llama-cpp/quarantine/:backend', async (c) => {
232
+ const backend = c.req.param('backend');
233
+ if (!isLlamaBackend(backend)) {
234
+ return c.json({ error: 'backend must be "cuda", "vulkan", "metal", or "cpu"' }, 400);
235
+ }
236
+ const override = (await ctx.store.readConfig()).llamaCppBackendOverride;
237
+ const { restored } = liftLlamaQuarantine(ctx.home, backend, {
238
+ pinned: override !== undefined && override !== 'auto',
239
+ });
240
+ if (restored) {
241
+ log.info(`llama-server ${backend} quarantine lifted; next launch uses ${restored.path}`);
242
+ // Engines resolve their binary when the provider is built, so a resident
243
+ // engine keeps running the fallback until it restarts. Same soft reset
244
+ // as the policies above: idle engines go now, busy ones after their turn.
245
+ invalidateModelsCache();
246
+ await ctx.chat.resetClient({ deferBusy: true });
247
+ }
248
+ return c.json(llamaQuarantineView(ctx.home));
249
+ });
215
250
  /**
216
251
  * Per-model context overrides (Settings → Local models → Context size…).
217
252
  * Engine-owner state like the sizing policy above — packaged user daemons
@@ -10,6 +10,7 @@ import { EXTERNAL_CONVERSATION_ID_HEADER, EXTERNAL_PROJECT_HEADER, EXTERNAL_WORK
10
10
  import { appendCallerOwnedActionLedger } from '../openai-compat/action-ledger.js';
11
11
  import { profileForCallerOwnedInference } from '../openai-compat/caller-owned-profile.js';
12
12
  import { resolveChatTarget, resolveFallbackGezelId, resolvePinnedModelTarget, } from '../openai-compat/chat-target.js';
13
+ import { engineFailure } from '../openai-compat/engine-failure.js';
13
14
  import { overlayOpenAiRequestTuning } from '../openai-compat/request-tuning.js';
14
15
  import { createStreamingDiagnostics, snapshotStreamingDiagnostics, } from '../openai-compat/streaming-telemetry.js';
15
16
  import { runNonStreaming, runStreaming } from '../openai-compat/streaming.js';
@@ -536,12 +537,13 @@ export function v1ChatRoutes(ctx, opts = {}) {
536
537
  clientAborted,
537
538
  error: describeErrorChain(err),
538
539
  })}`);
540
+ const failure = engineFailure(err);
539
541
  await stream
540
542
  .writeSSE({
541
543
  data: JSON.stringify({
542
544
  error: err instanceof UnsupportedResponseFormatError
543
545
  ? { message, type: 'invalid_request_error', code: err.code }
544
- : { message, type: 'server_error', code: 'provider_error' },
546
+ : { message, type: failure.type, code: failure.code },
545
547
  }),
546
548
  })
547
549
  .catch((sseErr) => {
@@ -644,6 +646,13 @@ export function v1ChatRoutes(ctx, opts = {}) {
644
646
  }, 400);
645
647
  }
646
648
  const message = err instanceof Error ? err.message : String(err);
649
+ // Before the prose checks below: an engine's own message can contain
650
+ // "empty", and a typed failure says more than any substring does.
651
+ const failure = engineFailure(err);
652
+ if (failure.code !== 'provider_error') {
653
+ log.warn(`chat completion failed (${failure.code}): ${message}`);
654
+ return c.json({ error: { message, type: failure.type, code: failure.code } }, failure.status);
655
+ }
647
656
  if (message.includes('messages must') ||
648
657
  message.includes('empty') ||
649
658
  message.includes('cannot be an assistant turn')) {
@@ -1,6 +1,7 @@
1
- import { estimateManifestKvBytes, externalGezelModelId, isRecommendedModel, localContextFloorTokens, rateModelsForDevice, resolveDistributionProfile, } from '@bendyline/gezel';
1
+ import { estimateManifestKvBytes, externalGezelModelId, isRecommendedModel, llamaCppBackendExclusion, localContextFloorTokens, rateModelsForDevice, resolveDistributionProfile, } from '@bendyline/gezel';
2
2
  import { resolveOnDeviceProvider } from '@bendyline/gezel/native';
3
3
  import { Hono } from 'hono';
4
+ import { currentLlamaCppBackend } from '../../providers/llama-cpp/backend-health.js';
4
5
  import { CapacityBroker } from '../../providers/native/capacity-broker.js';
5
6
  import { detectMemoryProfileCached } from '../../system/memory.js';
6
7
  import { appleSystemModel } from '../openai-compat/apple-model.js';
@@ -59,14 +60,15 @@ function catalogBackend(manifest) {
59
60
  // Store hosts have no provisioned Python runtime. Only advertise models
60
61
  // backed by bundled native engines until a frozen MLX runtime ships.
61
62
  const store = resolveDistributionProfile().pythonProvisioning === 'frozen-only';
63
+ const llamaCpp = manifest.llamaCpp && !llamaCppBackendExclusion(manifest.llamaCpp, currentLlamaCppBackend());
62
64
  if (store)
63
- return manifest.llamaCpp ? 'llama-cpp' : ds4;
65
+ return llamaCpp ? 'llama-cpp' : ds4;
64
66
  const preferred = resolveOnDeviceProvider(process.platform, process.arch);
65
67
  if (preferred === 'mlx' && manifest.mlx && !manifest.mlx.disabledReason)
66
68
  return 'mlx';
67
- if (preferred === 'llama-cpp' && manifest.llamaCpp)
69
+ if (preferred === 'llama-cpp' && llamaCpp)
68
70
  return 'llama-cpp';
69
- if (manifest.llamaCpp)
71
+ if (llamaCpp)
70
72
  return 'llama-cpp';
71
73
  if (manifest.mlx && !manifest.mlx.disabledReason && process.platform === 'darwin')
72
74
  return 'mlx';
@@ -8,6 +8,7 @@ import { ExternalToolsUnsupportedError, ModelNotInstalledError, } from '../../pr
8
8
  import { appendCallerOwnedActionLedger } from '../openai-compat/action-ledger.js';
9
9
  import { profileForCallerOwnedInference } from '../openai-compat/caller-owned-profile.js';
10
10
  import { resolveChatTarget, resolvePinnedModelTarget, } from '../openai-compat/chat-target.js';
11
+ import { engineFailure } from '../openai-compat/engine-failure.js';
11
12
  import { runResponsesNonStreaming, runResponsesStreaming, } from '../openai-compat/responses-streaming.js';
12
13
  import { ResponsesRequestSchema, translateResponsesRequest, } from '../openai-compat/responses-translate.js';
13
14
  import { flattenTranscriptIntoPrompt, parseGezelModelRef, resolveModelTarget, } from '../openai-compat/translate.js';
@@ -221,7 +222,7 @@ export function v1ResponsesRoutes(ctx, opts = {}) {
221
222
  event: 'error',
222
223
  data: JSON.stringify({
223
224
  type: 'error',
224
- code: 'provider_error',
225
+ code: engineFailure(err).code,
225
226
  message: 'The response stream ended unexpectedly.',
226
227
  param: null,
227
228
  }),
@@ -264,6 +265,11 @@ export function v1ResponsesRoutes(ctx, opts = {}) {
264
265
  return invalidRequest(c, err.message, err.code);
265
266
  }
266
267
  const message = err instanceof Error ? err.message : String(err);
268
+ const failure = engineFailure(err);
269
+ if (failure.code !== 'provider_error') {
270
+ log.warn(`Responses request failed (${failure.code}): ${message}`);
271
+ return c.json({ error: { message, type: failure.type, code: failure.code } }, failure.status);
272
+ }
267
273
  if (message.includes('messages must') ||
268
274
  message.includes('empty') ||
269
275
  message.includes('cannot be an assistant turn')) {
@@ -7,7 +7,7 @@
7
7
  * (docs/gezk-format.md). The in-process implementation is test/CLI-only;
8
8
  * production fails closed if its worker becomes unavailable.
9
9
  */
10
- import type { CatalogAssetFile, CatalogAssetInfo, CatalogAssetRead, CatalogChunkHit, CatalogDocumentMeta, CatalogTopic, CatalogValidationReport } from '@bendyline/gezel-knowledge';
10
+ import type { CatalogAssetFile, CatalogAssetInfo, CatalogAssetRead, CatalogChunkHit, CatalogDocumentInitial, CatalogDocumentMeta, CatalogTopic, CatalogValidationReport } from '@bendyline/gezel-knowledge';
11
11
  import type { KnowledgeRadius } from '@bendyline/gezel';
12
12
  import type { SpatialMatch } from '@bendyline/gezel-knowledge';
13
13
  export interface MountSpec {
@@ -78,6 +78,14 @@ export interface KnowledgeCatalogHost {
78
78
  documents: CatalogDocumentMeta[];
79
79
  total: number;
80
80
  }>;
81
+ documentInitials(key: string, opts: {
82
+ topicId?: string;
83
+ descendants?: boolean;
84
+ }): Promise<CatalogDocumentInitial[]>;
85
+ documentPosition(key: string, documentId: string, opts: {
86
+ topicId?: string;
87
+ descendants?: boolean;
88
+ }): Promise<number | null>;
81
89
  nearbyDocuments(key: string, radius: KnowledgeRadius, opts: {
82
90
  offset?: number;
83
91
  limit?: number;
@@ -158,6 +158,8 @@ export async function createInProcessCatalogHost() {
158
158
  validate: async (rootDir, deep) => validateExtractedCatalog(rootDir, { deep }),
159
159
  topics: async (key) => mustGet(handles, key).topics(),
160
160
  documentsPage: async (key, opts) => mustGet(handles, key).documentsPage(opts),
161
+ documentInitials: async (key, opts) => mustGet(handles, key).documentInitials(opts),
162
+ documentPosition: async (key, documentId, opts) => mustGet(handles, key).documentPosition(documentId, opts),
161
163
  nearbyDocuments: async (key, radius, opts) => mustGet(handles, key).nearbyDocuments(radius, opts),
162
164
  getDocument: async (key, documentId) => mustGet(handles, key).getDocument(documentId),
163
165
  getChunk: async (key, documentId, chunkUid) => mustGet(handles, key).getChunk(documentId, chunkUid),
@@ -121,6 +121,12 @@ export declare class KnowledgeManager {
121
121
  * what the Knowledge screen's browser renders. Offline-safe: gilde is local.
122
122
  */
123
123
  available(): Promise<KnowledgeAvailableCatalog[]>;
124
+ /**
125
+ * The search model an install of a catalog with this profile would also
126
+ * download. None when the catalog searches with the daemon's own embedder
127
+ * (an install never fetches that) or the profile's pinned files are cached.
128
+ */
129
+ private searchModelDownloadBytes;
124
130
  /** Resolve a catalogId (URI authority) to its mounted info, or null. */
125
131
  mountedCatalog(catalogId: string): MountedInfo | null;
126
132
  /**
@@ -206,6 +212,14 @@ export declare class KnowledgeManager {
206
212
  limit?: number;
207
213
  descendants?: boolean;
208
214
  }): ReturnType<KnowledgeCatalogHost['documentsPage']>;
215
+ documentInitials(catalogId: string, opts: {
216
+ topicId?: string;
217
+ descendants?: boolean;
218
+ }): ReturnType<KnowledgeCatalogHost['documentInitials']>;
219
+ documentPosition(catalogId: string, documentId: string, opts: {
220
+ topicId?: string;
221
+ descendants?: boolean;
222
+ }): ReturnType<KnowledgeCatalogHost['documentPosition']>;
209
223
  getDocument(catalogId: string, documentId: string): ReturnType<KnowledgeCatalogHost['getDocument']>;
210
224
  /** One passage by its citation id, with the document's title and origin. */
211
225
  getPassage(catalogId: string, documentId: string, chunkUid: string): Promise<KnowledgePassageRead | null>;
@@ -13,7 +13,7 @@ import { readFile, readdir, rm, stat } from 'node:fs/promises';
13
13
  import { dirname, join, resolve } from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  import { KnowledgeMachineInventorySchema, KnowledgeNearbyRequestSchema, createLogger, formatKnowledgeUri, parseKnowledgeUri, resolveSecurityPolicy, sameVectorSpace, } from '@bendyline/gezel';
16
- import { compareCatalogVersions, knowledgeEmbeddingProfile, readGezkManifest, } from '@bendyline/gezel-knowledge';
16
+ import { compareCatalogVersions, knowledgeEmbeddingProfile, knowledgeProfileQueryModelBytes, missingPinnedFiles, readGezkManifest, } from '@bendyline/gezel-knowledge';
17
17
  import { knowledgeCatalogVersionDir, knowledgeCatalogsDir, knowledgeDownloadsDir, } from '@bendyline/gezel/paths';
18
18
  import { HANDBOEK_KNOWLEDGE_CATALOG, HANDBOEK_KNOWLEDGE_PUBLISHER } from './bundled-catalog.js';
19
19
  import { scoreResult } from '@bendyline/gezel';
@@ -21,10 +21,11 @@ import { sharedKnowledgeRoot, sharedKnowledgeVersionDir, } from '../machine-engi
21
21
  import { embedKnowledgeQuery, sharesDaemonEmbedder } from '../memory/embeddings.js';
22
22
  import { ChatModelInstallRegistry } from '../models/install-registry.js';
23
23
  import { beginPerfWork } from '../perf/responsiveness.js';
24
+ import { HF_CACHE_DIR_ENV, transformersCacheDir } from '../transformers-cache.js';
24
25
  import { resolveKnowledgeCatalogSource } from './catalog-source.js';
25
26
  import { KNOWLEDGE_DOWNLOAD_KEY_PATTERN, installKnowledgeCatalog, knowledgeDownloadKey, pruneOtherKnowledgeCatalogVersions, } from './install.js';
26
27
  import { KnowledgeRegistry } from './registry.js';
27
- import { KnowledgeSpatialCursorError, encodeNearbyCursor, nearbyCursorOffset, regionalDocumentIdentity, } from './spatial-query.js';
28
+ import { KnowledgeSpatialCursorError, encodeNearbyCursor, nearbyCursorOffset, spatialDocumentIdentity, } from './spatial-query.js';
28
29
  import { resolveKnowledgeVectorFloors } from './vector-floors.js';
29
30
  const log = createLogger('knowledge');
30
31
  /** Explicit-search shard budget (S) across every active catalog (§3.4). */
@@ -59,6 +60,9 @@ function semanticSearchModeFor(profile) {
59
60
  return 'profile';
60
61
  return 'keyword-only';
61
62
  }
63
+ function searchModelOf(profile) {
64
+ return { profileId: profile.id, repo: profile.model.repo };
65
+ }
62
66
  /**
63
67
  * Explicit search fuses three arms per catalog by reciprocal rank (k = 60):
64
68
  * the vector chunks (best chunk per document, by rerank cosine), the
@@ -409,10 +413,18 @@ export class KnowledgeManager {
409
413
  const shared = await this.sharedInventoryKeys();
410
414
  const activeIds = new Set(this.installs.active().map((a) => a.id));
411
415
  const partials = await this.partialDownloadKeys();
416
+ const modelBytes = new Map();
417
+ for (const item of gilde.values()) {
418
+ const profileId = item.embeddingProfile.id;
419
+ if (!modelBytes.has(profileId)) {
420
+ modelBytes.set(profileId, await this.searchModelDownloadBytes(profileId));
421
+ }
422
+ }
412
423
  return [...gilde.values()]
413
424
  .sort((a, b) => a.name.localeCompare(b.name))
414
425
  .map((item) => {
415
426
  const entry = registry.find((c) => c.ref.publisherId === item.publisherId && c.ref.catalogId === item.id);
427
+ const searchModelDownloadBytes = modelBytes.get(item.embeddingProfile.id);
416
428
  return {
417
429
  id: item.id,
418
430
  publisherId: item.publisherId,
@@ -451,9 +463,24 @@ export class KnowledgeManager {
451
463
  sharedOnDevice: shared.has(`${item.publisherId}/${item.id}/${item.version}/${item.sha256}`),
452
464
  installing: activeIds.has(item.id),
453
465
  incompleteDownload: partials.has(item.sha256.slice(0, 16)),
466
+ ...(searchModelDownloadBytes ? { searchModelDownloadBytes } : {}),
454
467
  };
455
468
  });
456
469
  }
470
+ /**
471
+ * The search model an install of a catalog with this profile would also
472
+ * download. None when the catalog searches with the daemon's own embedder
473
+ * (an install never fetches that) or the profile's pinned files are cached.
474
+ */
475
+ async searchModelDownloadBytes(profileId) {
476
+ const profile = knowledgeEmbeddingProfile(profileId);
477
+ const bytes = knowledgeProfileQueryModelBytes(profileId);
478
+ if (!profile || bytes === null || semanticSearchModeFor(profile) !== 'profile')
479
+ return undefined;
480
+ const cacheDir = process.env[HF_CACHE_DIR_ENV] ?? transformersCacheDir(this.opts.home);
481
+ const missing = await missingPinnedFiles(profile, { cacheDir });
482
+ return missing.length > 0 ? bytes : undefined;
483
+ }
457
484
  /** Resolve a catalogId (URI authority) to its mounted info, or null. */
458
485
  mountedCatalog(catalogId) {
459
486
  for (const info of this.mountedByKey.values()) {
@@ -676,7 +703,13 @@ export class KnowledgeManager {
676
703
  let warning;
677
704
  const mounted = this.mountedByKey.get(key);
678
705
  if (mounted?.semanticSearch === 'profile') {
679
- yield { type: 'progress', phase: 'embedder', bytesDone: 0, bytesTotal: 0 };
706
+ yield {
707
+ type: 'progress',
708
+ phase: 'embedder',
709
+ bytesDone: 0,
710
+ bytesTotal: 0,
711
+ searchModel: searchModelOf(mounted.embedding),
712
+ };
680
713
  warning = yield* this.prewarmProfile(mounted.embedding);
681
714
  }
682
715
  yield { ...event, ...(warning ? { warning } : {}) };
@@ -746,7 +779,12 @@ export class KnowledgeManager {
746
779
  (settled || Date.now() - lastReportAt >= EMBEDDER_PROGRESS_INTERVAL_MS)) {
747
780
  progress.reported = latest;
748
781
  lastReportAt = Date.now();
749
- yield { type: 'progress', phase: 'embedder', ...latest };
782
+ yield {
783
+ type: 'progress',
784
+ phase: 'embedder',
785
+ ...latest,
786
+ searchModel: searchModelOf(profile),
787
+ };
750
788
  continue;
751
789
  }
752
790
  if (settled)
@@ -802,6 +840,9 @@ export class KnowledgeManager {
802
840
  phase,
803
841
  bytesDone: lastEvent?.type === 'progress' ? lastEvent.bytesDone : 0,
804
842
  bytesTotal: lastEvent?.type === 'progress' ? lastEvent.bytesTotal : 0,
843
+ ...(lastEvent?.type === 'progress' && lastEvent.searchModel
844
+ ? { searchModel: lastEvent.searchModel }
845
+ : {}),
805
846
  };
806
847
  });
807
848
  }
@@ -1035,6 +1076,12 @@ export class KnowledgeManager {
1035
1076
  async documentsPage(catalogId, opts) {
1036
1077
  return this.opts.host.documentsPage(this.requireMounted(catalogId).key, opts);
1037
1078
  }
1079
+ async documentInitials(catalogId, opts) {
1080
+ return this.opts.host.documentInitials(this.requireMounted(catalogId).key, opts);
1081
+ }
1082
+ async documentPosition(catalogId, documentId, opts) {
1083
+ return this.opts.host.documentPosition(this.requireMounted(catalogId).key, documentId, opts);
1084
+ }
1038
1085
  async getDocument(catalogId, documentId) {
1039
1086
  return this.opts.host.getDocument(this.requireMounted(catalogId).key, documentId);
1040
1087
  }
@@ -1145,7 +1192,7 @@ export class KnowledgeManager {
1145
1192
  documentId: document.id,
1146
1193
  }),
1147
1194
  };
1148
- const identity = regionalDocumentIdentity(info.ref.publisherId, info.ref.catalogId, document.id);
1195
+ const identity = spatialDocumentIdentity(info.ref.publisherId, info.ref.catalogId, document.id);
1149
1196
  const previous = unique.get(identity);
1150
1197
  if (!previous || result.distanceMeters < previous.distanceMeters)
1151
1198
  unique.set(identity, result);
@@ -1383,7 +1430,7 @@ export class KnowledgeManager {
1383
1430
  const count = perCatalogCount.get(entry.catalogKey) ?? 0;
1384
1431
  if (capPerCatalog && count >= PER_CATALOG_CAP)
1385
1432
  continue;
1386
- const spatialIdentity = regionalDocumentIdentity(info.ref.publisherId, info.ref.catalogId, entry.documentId);
1433
+ const spatialIdentity = spatialDocumentIdentity(info.ref.publisherId, info.ref.catalogId, entry.documentId);
1387
1434
  if (opts.spatial && seenSpatial.has(spatialIdentity))
1388
1435
  continue;
1389
1436
  const relevance = fusedRankRelevance(out.length);
@@ -12,6 +12,6 @@
12
12
  */
13
13
  export interface KnowledgeWorkerRequest {
14
14
  id: number;
15
- op: 'mount' | 'unmount' | 'mounted' | 'prewarm' | 'validate' | 'topics' | 'documentsPage' | 'nearbyDocuments' | 'getDocument' | 'getChunk' | 'assets' | 'readAsset' | 'assetFile' | 'search' | 'dispose';
15
+ op: 'mount' | 'unmount' | 'mounted' | 'prewarm' | 'validate' | 'topics' | 'documentsPage' | 'documentInitials' | 'documentPosition' | 'nearbyDocuments' | 'getDocument' | 'getChunk' | 'assets' | 'readAsset' | 'assetFile' | 'search' | 'dispose';
16
16
  args: unknown[];
17
17
  }
@@ -1,11 +1,10 @@
1
1
  /**
2
2
  * Radius pagination binds offsets to the authorized catalog snapshot and
3
3
  * query. Cursors confer no authority; the manager recalculates scope on
4
- * every request. Regional replicas share identity only under Qualla's
5
- * declared regional catalog and source-document naming convention.
4
+ * every request.
6
5
  */
7
6
  export declare class KnowledgeSpatialCursorError extends Error {
8
7
  }
9
8
  export declare function encodeNearbyCursor(snapshot: string, offset: number): string;
10
9
  export declare function nearbyCursorOffset(cursor: string | undefined, snapshot: string): number;
11
- export declare function regionalDocumentIdentity(publisherId: string, catalogId: string, documentId: string): string;
10
+ export declare function spatialDocumentIdentity(publisherId: string, catalogId: string, documentId: string): string;