@bendyline/gezel-inference 1.0.0 → 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.
- package/README.md +90 -8
- package/dist/cache/adapter.d.ts +7 -0
- package/dist/cache/controller.d.ts +12 -0
- package/dist/cache/controller.js +24 -0
- package/dist/catalog/chat-models/bt/btl4-35b-q4/manifest.json +1 -0
- package/dist/catalog/chat-models/gl/glm-5.2-754b-q2/manifest.json +1 -0
- package/dist/catalog/chat-models/index.json +1 -1
- package/dist/catalog/chat-models/lf/lfm2.5-24b-a2b-q4/manifest.json +122 -0
- package/dist/catalog/chat-models/lf/lfm2.5-24b-a2b-q4/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/chat-models/mi/minicpm-v4.7-35b-a3b-q4/manifest.json +185 -0
- package/dist/catalog/chat-models/mi/minicpm-v4.7-35b-a3b-q4/versions/1.0.0/manifest.json +94 -0
- package/dist/catalog/chat-models/mi/minicpm5-1b-q4/manifest.json +1 -0
- package/dist/catalog/chat-models/mi/minicpm5-2b-q4/manifest.json +1 -0
- package/dist/catalog/chat-models/qw/qwen3.5-0.8b-q4/manifest.json +1 -0
- package/dist/catalog/chat-models/raw-index.json +1 -1
- package/dist/catalog/provenance.json +1 -1
- package/dist/engines/electron-native-reuse.d.ts +9 -0
- package/dist/engines/electron-native-reuse.js +146 -45
- package/dist/engines/native-file-manifest.json +404 -404
- package/dist/engines/native-manifest.d.ts +11 -3
- package/dist/engines/native-manifest.js +48 -18
- package/dist/http/openai-compat/apple-model.d.ts +2 -0
- package/dist/http/openai-compat/apple-model.js +21 -1
- package/dist/http/openai-compat/chat-target.d.ts +12 -0
- package/dist/http/openai-compat/chat-target.js +27 -1
- package/dist/http/openai-compat/engine-failure.d.ts +25 -0
- package/dist/http/openai-compat/engine-failure.js +76 -0
- package/dist/http/openai-compat/responses-streaming.js +2 -1
- package/dist/http/openai-compat/responses-translate.d.ts +1 -1
- package/dist/http/openai-compat/translate.d.ts +1 -1
- package/dist/http/routes/engines.js +44 -3
- package/dist/http/routes/v1-chat.js +30 -8
- package/dist/http/routes/v1-models.d.ts +6 -0
- package/dist/http/routes/v1-models.js +137 -30
- package/dist/http/routes/v1-responses.js +16 -3
- package/dist/in-flight-requests.d.ts +19 -0
- package/dist/in-flight-requests.js +34 -0
- package/dist/inference-service.js +3 -3
- package/dist/knowledge/catalog-host.d.ts +9 -1
- package/dist/knowledge/catalog-host.js +2 -0
- package/dist/knowledge/manager.d.ts +14 -0
- package/dist/knowledge/manager.js +53 -6
- package/dist/knowledge/search-worker.d.ts +1 -1
- package/dist/knowledge/spatial-query.d.ts +2 -3
- package/dist/knowledge/spatial-query.js +12 -6
- package/dist/knowledge/worker-host.js +2 -0
- package/dist/memory/embed-core.d.ts +16 -0
- package/dist/memory/embed-core.js +31 -0
- package/dist/memory/embed-worker.d.ts +7 -3
- package/dist/memory/embed-worker.js +13 -4
- package/dist/memory/embeddings.d.ts +17 -2
- package/dist/memory/embeddings.js +36 -7
- package/dist/models/ensure.d.ts +2 -0
- package/dist/models/ensure.js +9 -0
- package/dist/perf/responsiveness.d.ts +14 -1
- package/dist/perf/responsiveness.js +77 -7
- package/dist/providers/llama-cpp/backend-health.d.ts +30 -0
- package/dist/providers/llama-cpp/backend-health.js +64 -0
- package/dist/providers/llama-cpp/backend-quarantine.d.ts +40 -0
- package/dist/providers/llama-cpp/backend-quarantine.js +100 -0
- package/dist/providers/llama-cpp/build-provider.js +140 -118
- package/dist/providers/llama-cpp/cache-adapter.d.ts +12 -0
- package/dist/providers/llama-cpp/cache-adapter.js +36 -1
- package/dist/providers/llama-cpp/devices.d.ts +11 -2
- package/dist/providers/llama-cpp/devices.js +10 -4
- package/dist/providers/llama-cpp/engine-flags.d.ts +0 -18
- package/dist/providers/llama-cpp/engine-flags.js +12 -18
- package/dist/providers/llama-cpp/gguf-metadata-async.js +7 -0
- package/dist/providers/llama-cpp/gguf-metadata.js +20 -3
- package/dist/providers/llama-cpp/gpu-startup-watch.d.ts +41 -0
- package/dist/providers/llama-cpp/gpu-startup-watch.js +58 -0
- package/dist/providers/llama-cpp/models.d.ts +6 -0
- package/dist/providers/llama-cpp/models.js +19 -1
- package/dist/providers/llama-cpp/offload-planner.d.ts +3 -18
- package/dist/providers/llama-cpp/offload-planner.js +3 -14
- package/dist/providers/llama-cpp/provider.d.ts +8 -0
- package/dist/providers/llama-cpp/provider.js +4 -1
- package/dist/providers/mlx/provider.js +14 -11
- package/dist/providers/mlx/python/batch_processors.py +26 -0
- package/dist/providers/mlx/python/gezel_mlx_server.py +88 -20
- package/dist/providers/mlx/python/qwen4_ple.py +9 -2
- package/dist/providers/mlx/python/response_format.py +374 -0
- package/dist/providers/mlx/python/spec_decode.py +5 -0
- package/dist/providers/mlx/python/think_budget.py +35 -1
- package/dist/providers/mlx/python/tool_grammar.py +62 -3
- package/dist/providers/mlx/reasoning-stream.d.ts +20 -2
- package/dist/providers/mlx/reasoning-stream.js +77 -16
- package/dist/providers/mlx/runtime-diagnostics.d.ts +16 -0
- package/dist/providers/mlx/runtime-diagnostics.js +29 -2
- package/dist/providers/native/capacity-broker.d.ts +51 -10
- package/dist/providers/native/capacity-broker.js +80 -9
- package/dist/providers/native/capacity-budget.d.ts +35 -3
- package/dist/providers/native/capacity-budget.js +49 -5
- package/dist/providers/native/device-capacity.d.ts +1 -1
- package/dist/providers/native/device-capacity.js +23 -5
- package/dist/providers/native/local-engine-runtime.js +15 -13
- package/dist/providers/native/measured-budget.js +2 -0
- package/dist/providers/native/supervisor.d.ts +25 -0
- package/dist/providers/native/supervisor.js +59 -7
- package/dist/remotes/identity.d.ts +1 -0
- package/dist/remotes/identity.js +6 -0
- package/dist/remotes/pinned-fetch.d.ts +2 -0
- package/dist/remotes/pinned-fetch.js +30 -1
- package/dist/remotes/types.d.ts +14 -0
- package/dist/runtime-lock.js +1 -1
- package/dist/system/amdgpu-adapters.d.ts +18 -0
- package/dist/system/amdgpu-adapters.js +55 -0
- package/dist/system/gpu-carve-out.d.ts +84 -0
- package/dist/system/gpu-carve-out.js +111 -0
- package/dist/system/memory.d.ts +17 -1
- package/dist/system/memory.js +105 -14
- package/dist/transformers-cache.d.ts +2 -0
- package/dist/transformers-cache.js +6 -3
- package/dist/utils/service-worker-entry.d.ts +4 -0
- package/dist/utils/service-worker-entry.js +4 -0
- package/package.json +14 -10
|
@@ -40,17 +40,25 @@
|
|
|
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.
|
|
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 = "
|
|
46
|
-
/** Exact SHA256 values for every engine archive published by native-v0.1.
|
|
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.51. */
|
|
49
|
+
export declare const NATIVE_ENGINE_ARCHIVE_BYTES: Readonly<Record<string, number>>;
|
|
48
50
|
/**
|
|
49
51
|
* True only when the pinned release's standalone macOS archives were
|
|
50
52
|
* Developer ID signed and accepted by Apple's notary service before
|
|
51
53
|
* packaging. Electron notarization is a separate distribution contract.
|
|
52
54
|
*/
|
|
53
55
|
export declare const NATIVE_ENGINE_MACOS_NOTARIZED = true;
|
|
56
|
+
/**
|
|
57
|
+
* The download size of the archive the resolver fetches for this platform
|
|
58
|
+
* and variant, when this build pins it. Undefined under a dev release
|
|
59
|
+
* override, whose archives this build has no sizes for.
|
|
60
|
+
*/
|
|
61
|
+
export declare function pinnedEngineArchiveBytes(platformKey: string, variant: string | undefined, platform?: NodeJS.Platform): number | undefined;
|
|
54
62
|
/** True when a sha256 hex string is the all-zeros placeholder. */
|
|
55
63
|
export declare function isPlaceholderDigest(digest: string): boolean;
|
|
56
64
|
/**
|
|
@@ -40,27 +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.
|
|
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 = '
|
|
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.
|
|
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.
|
|
50
|
-
'gezel-native-0.1.
|
|
51
|
-
'gezel-native-0.1.
|
|
52
|
-
'gezel-native-0.1.
|
|
53
|
-
'gezel-native-0.1.
|
|
54
|
-
'gezel-native-0.1.
|
|
55
|
-
'gezel-native-0.1.
|
|
56
|
-
'gezel-native-0.1.
|
|
57
|
-
'gezel-native-0.1.
|
|
58
|
-
'gezel-native-0.1.
|
|
59
|
-
'gezel-native-0.1.
|
|
60
|
-
'gezel-native-0.1.
|
|
61
|
-
'gezel-native-0.1.
|
|
62
|
-
'gezel-native-0.1.
|
|
63
|
-
'gezel-native-0.1.
|
|
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
|
+
});
|
|
65
|
+
/** Download size in bytes of every engine archive published by native-v0.1.51. */
|
|
66
|
+
export const NATIVE_ENGINE_ARCHIVE_BYTES = Object.freeze({
|
|
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,
|
|
64
82
|
});
|
|
65
83
|
// END PINNED NATIVE ARCHIVE HASHES
|
|
66
84
|
/**
|
|
@@ -69,6 +87,18 @@ export const NATIVE_ENGINE_ARCHIVE_SHA256 = Object.freeze({
|
|
|
69
87
|
* packaging. Electron notarization is a separate distribution contract.
|
|
70
88
|
*/
|
|
71
89
|
export const NATIVE_ENGINE_MACOS_NOTARIZED = true;
|
|
90
|
+
/**
|
|
91
|
+
* The download size of the archive the resolver fetches for this platform
|
|
92
|
+
* and variant, when this build pins it. Undefined under a dev release
|
|
93
|
+
* override, whose archives this build has no sizes for.
|
|
94
|
+
*/
|
|
95
|
+
export function pinnedEngineArchiveBytes(platformKey, variant, platform = process.platform) {
|
|
96
|
+
if (effectiveEngineRelease() !== NATIVE_ENGINE_RELEASE)
|
|
97
|
+
return undefined;
|
|
98
|
+
const key = variant ? `${platformKey}-${variant}` : platformKey;
|
|
99
|
+
const ext = platform === 'win32' ? 'zip' : 'tar.gz';
|
|
100
|
+
return NATIVE_ENGINE_ARCHIVE_BYTES[`gezel-native-${NATIVE_ENGINE_RELEASE}-${key}.${ext}`];
|
|
101
|
+
}
|
|
72
102
|
/** True when a sha256 hex string is the all-zeros placeholder. */
|
|
73
103
|
export function isPlaceholderDigest(digest) {
|
|
74
104
|
return /^0{64}$/.test(digest.trim().toLowerCase());
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { AppModel } from '@bendyline/gezel/app-models';
|
|
2
|
+
/** @internal Lets tests list models under a changed Apple status. */
|
|
3
|
+
export declare function resetAppleStatusCache(): void;
|
|
2
4
|
/** Inventory owns system readiness, so embedders never speak the native helper protocol. */
|
|
3
5
|
export declare function appleSystemModel(created: number): Promise<AppModel[]>;
|
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
import { appleFoundationModelsStatus } from '../../providers/apple-foundation-models/status.js';
|
|
2
|
+
/**
|
|
3
|
+
* How long listings reuse Apple's readiness. Each check launches the
|
|
4
|
+
* `gezel-apple-fm` helper for one handshake (26-59 ms measured), and the SDK's
|
|
5
|
+
* `streamText` lists models before every call, so a fresh check per listing
|
|
6
|
+
* cost a process launch per chat. Short enough that Apple Intelligence
|
|
7
|
+
* enabled in System Settings shows up almost at once.
|
|
8
|
+
*/
|
|
9
|
+
const STATUS_TTL_MS = 10_000;
|
|
10
|
+
let cachedStatus = null;
|
|
11
|
+
/** Concurrent listings share one in-flight check. */
|
|
12
|
+
function appleStatus() {
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
if (!cachedStatus || now - cachedStatus.at > STATUS_TTL_MS)
|
|
15
|
+
cachedStatus = { at: now, status: appleFoundationModelsStatus() };
|
|
16
|
+
return cachedStatus.status;
|
|
17
|
+
}
|
|
18
|
+
/** @internal Lets tests list models under a changed Apple status. */
|
|
19
|
+
export function resetAppleStatusCache() {
|
|
20
|
+
cachedStatus = null;
|
|
21
|
+
}
|
|
2
22
|
/** Inventory owns system readiness, so embedders never speak the native helper protocol. */
|
|
3
23
|
export async function appleSystemModel(created) {
|
|
4
|
-
const status = await
|
|
24
|
+
const status = await appleStatus();
|
|
5
25
|
if (!status.supported || !status.installed)
|
|
6
26
|
return [];
|
|
7
27
|
return [
|
|
@@ -43,6 +43,18 @@ export type ChatTargetInput = {
|
|
|
43
43
|
provider: ProviderName;
|
|
44
44
|
model: string | undefined;
|
|
45
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* The on-device model a bare id names, when this install pins it as a default.
|
|
48
|
+
*
|
|
49
|
+
* `ensureModel` hands an app the bare id it asked for (`qwen3.5-0.8b-q4`) and
|
|
50
|
+
* pins it; an inference-only host, which has no gezel to fall back to, then
|
|
51
|
+
* answered that id with "Unknown model". Only a pin is trusted, so an unpinned
|
|
52
|
+
* bare id still reaches the serving gezel like any other unknown string. When
|
|
53
|
+
* several engines pin the same id, the configured provider decides. A cloud
|
|
54
|
+
* provider's pin never matches: an app's hardcoded `gpt-4o` must reach the
|
|
55
|
+
* serving gezel, not leave the machine because it equals the OpenAI default.
|
|
56
|
+
*/
|
|
57
|
+
export declare function resolvePinnedModelTarget(ctx: ServiceContext, model: string): Promise<ChatTarget | null>;
|
|
46
58
|
/**
|
|
47
59
|
* Pick the gezel that guarantees a persona-backed fallback for public
|
|
48
60
|
* inference requests.
|
|
@@ -1,4 +1,30 @@
|
|
|
1
|
-
import { externalGezelModelId } from '@bendyline/gezel';
|
|
1
|
+
import { externalGezelModelId, isLocalProvider } from '@bendyline/gezel';
|
|
2
|
+
import { resolveModelTarget } from './translate.js';
|
|
3
|
+
/**
|
|
4
|
+
* The on-device model a bare id names, when this install pins it as a default.
|
|
5
|
+
*
|
|
6
|
+
* `ensureModel` hands an app the bare id it asked for (`qwen3.5-0.8b-q4`) and
|
|
7
|
+
* pins it; an inference-only host, which has no gezel to fall back to, then
|
|
8
|
+
* answered that id with "Unknown model". Only a pin is trusted, so an unpinned
|
|
9
|
+
* bare id still reaches the serving gezel like any other unknown string. When
|
|
10
|
+
* several engines pin the same id, the configured provider decides. A cloud
|
|
11
|
+
* provider's pin never matches: an app's hardcoded `gpt-4o` must reach the
|
|
12
|
+
* serving gezel, not leave the machine because it equals the OpenAI default.
|
|
13
|
+
*/
|
|
14
|
+
export async function resolvePinnedModelTarget(ctx, model) {
|
|
15
|
+
const id = model.trim();
|
|
16
|
+
if (!id)
|
|
17
|
+
return null;
|
|
18
|
+
const config = await ctx.store.readConfig().catch(() => null);
|
|
19
|
+
const pins = Object.entries(config?.defaultModel ?? {}).filter(([provider, pinned]) => pinned === id &&
|
|
20
|
+
isLocalProvider(provider) &&
|
|
21
|
+
resolveModelTarget(`${provider}:${id}`) !== null);
|
|
22
|
+
const chosen = pins.find(([provider]) => provider === config?.provider) ??
|
|
23
|
+
(pins.length === 1 ? pins[0] : undefined);
|
|
24
|
+
if (!chosen)
|
|
25
|
+
return null;
|
|
26
|
+
return { provider: chosen[0], model: id, systemPrefix: '' };
|
|
27
|
+
}
|
|
2
28
|
/**
|
|
3
29
|
* Pick the gezel that guarantees a persona-backed fallback for public
|
|
4
30
|
* inference requests.
|
|
@@ -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.',
|
|
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<{
|
|
@@ -4,8 +4,10 @@ import { resolvePlatformKey } from '@bendyline/gezel/native';
|
|
|
4
4
|
import { Hono } from 'hono';
|
|
5
5
|
import { streamSSE } from 'hono/streaming';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
-
import { effectiveEngineRelease, isEnginePinned } from '../../engines/native-manifest.js';
|
|
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';
|
|
@@ -39,17 +41,23 @@ export function enginesRoutes(ctx) {
|
|
|
39
41
|
app.get('/binaries/status', (c) => {
|
|
40
42
|
const backendRaw = process.env.GEZEL_LLAMA_SERVER_BACKEND ?? process.env.GEZEL_LLAMA_DETECTED_BACKEND;
|
|
41
43
|
const llamaBackend = ['cuda', 'vulkan', 'metal', 'cpu'].find((value) => value === backendRaw);
|
|
44
|
+
const platformKey = resolvePlatformKey();
|
|
42
45
|
return c.json({
|
|
43
46
|
release: effectiveEngineRelease(),
|
|
44
47
|
pinned: isEnginePinned(),
|
|
45
|
-
platformKey
|
|
48
|
+
platformKey,
|
|
46
49
|
...(llamaBackend ? { llamaBackend } : {}),
|
|
47
50
|
engines: KNOWN_ENGINES.map((name) => {
|
|
48
51
|
const path = process.env[ENGINE_ENV_VAR[name]];
|
|
52
|
+
const installed = !!path && existsSync(path);
|
|
53
|
+
const downloadBytes = !installed && name === 'llama-server' && platformKey && llamaBackend
|
|
54
|
+
? pinnedEngineArchiveBytes(platformKey, llamaBackend)
|
|
55
|
+
: undefined;
|
|
49
56
|
return {
|
|
50
57
|
name,
|
|
51
|
-
installed
|
|
58
|
+
installed,
|
|
52
59
|
...(path ? { path } : {}),
|
|
60
|
+
...(downloadBytes ? { downloadBytes } : {}),
|
|
53
61
|
};
|
|
54
62
|
}),
|
|
55
63
|
});
|
|
@@ -206,6 +214,39 @@ export function enginesRoutes(ctx) {
|
|
|
206
214
|
}
|
|
207
215
|
return c.json({ policy: effective });
|
|
208
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
|
+
});
|
|
209
250
|
/**
|
|
210
251
|
* Per-model context overrides (Settings → Local models → Context size…).
|
|
211
252
|
* Engine-owner state like the sizing policy above — packaged user daemons
|
|
@@ -3,12 +3,14 @@ import { createLogger, isLocalProvider } from '@bendyline/gezel';
|
|
|
3
3
|
import { Hono } from 'hono';
|
|
4
4
|
import { streamSSE } from 'hono/streaming';
|
|
5
5
|
import { ZodError } from 'zod';
|
|
6
|
+
import { UnsupportedResponseFormatError } from '../../providers/mlx/runtime-diagnostics.js';
|
|
6
7
|
import { OLLAMA_TURN_TIMEOUT_MS } from '../../providers/turn-timeout.js';
|
|
7
8
|
import { ExternalToolsUnsupportedError, ModelNotInstalledError, } from '../../providers/types.js';
|
|
8
9
|
import { EXTERNAL_CONVERSATION_ID_HEADER, EXTERNAL_PROJECT_HEADER, EXTERNAL_WORKING_DIRECTORY_HEADER, } from '../external-conversation-headers.js';
|
|
9
10
|
import { appendCallerOwnedActionLedger } from '../openai-compat/action-ledger.js';
|
|
10
11
|
import { profileForCallerOwnedInference } from '../openai-compat/caller-owned-profile.js';
|
|
11
|
-
import { resolveChatTarget, resolveFallbackGezelId } from '../openai-compat/chat-target.js';
|
|
12
|
+
import { resolveChatTarget, resolveFallbackGezelId, resolvePinnedModelTarget, } from '../openai-compat/chat-target.js';
|
|
13
|
+
import { engineFailure } from '../openai-compat/engine-failure.js';
|
|
12
14
|
import { overlayOpenAiRequestTuning } from '../openai-compat/request-tuning.js';
|
|
13
15
|
import { createStreamingDiagnostics, snapshotStreamingDiagnostics, } from '../openai-compat/streaming-telemetry.js';
|
|
14
16
|
import { runNonStreaming, runStreaming } from '../openai-compat/streaming.js';
|
|
@@ -188,7 +190,11 @@ export function v1ChatRoutes(ctx, opts = {}) {
|
|
|
188
190
|
}
|
|
189
191
|
else {
|
|
190
192
|
const modelTarget = resolveModelTarget(parsed.model);
|
|
191
|
-
|
|
193
|
+
const pinnedTarget = modelTarget ? null : await resolvePinnedModelTarget(ctx, parsed.model);
|
|
194
|
+
if (pinnedTarget) {
|
|
195
|
+
target = pinnedTarget;
|
|
196
|
+
}
|
|
197
|
+
else if (!modelTarget) {
|
|
192
198
|
// Unknown model string (e.g. a client's hardcoded "gpt-4o").
|
|
193
199
|
// Route through the configured fallback gezel, or the Meester
|
|
194
200
|
// when no explicit override is set. Persona + tuning apply
|
|
@@ -199,7 +205,7 @@ export function v1ChatRoutes(ctx, opts = {}) {
|
|
|
199
205
|
if (!fallbackGezelId) {
|
|
200
206
|
return c.json({
|
|
201
207
|
error: {
|
|
202
|
-
message: `Unknown model "${parsed.model}", and no gezel is available to answer it.`,
|
|
208
|
+
message: `Unknown model "${parsed.model}", and no gezel is available to answer it. Use a <provider>:<model> id from GET /v1/models, such as "llama-cpp:${parsed.model}".`,
|
|
203
209
|
type: 'invalid_request_error',
|
|
204
210
|
code: 'gezel_not_found',
|
|
205
211
|
},
|
|
@@ -531,14 +537,13 @@ export function v1ChatRoutes(ctx, opts = {}) {
|
|
|
531
537
|
clientAborted,
|
|
532
538
|
error: describeErrorChain(err),
|
|
533
539
|
})}`);
|
|
540
|
+
const failure = engineFailure(err);
|
|
534
541
|
await stream
|
|
535
542
|
.writeSSE({
|
|
536
543
|
data: JSON.stringify({
|
|
537
|
-
error:
|
|
538
|
-
message,
|
|
539
|
-
type:
|
|
540
|
-
code: 'provider_error',
|
|
541
|
-
},
|
|
544
|
+
error: err instanceof UnsupportedResponseFormatError
|
|
545
|
+
? { message, type: 'invalid_request_error', code: err.code }
|
|
546
|
+
: { message, type: failure.type, code: failure.code },
|
|
542
547
|
}),
|
|
543
548
|
})
|
|
544
549
|
.catch((sseErr) => {
|
|
@@ -630,7 +635,24 @@ export function v1ChatRoutes(ctx, opts = {}) {
|
|
|
630
635
|
},
|
|
631
636
|
}, 400);
|
|
632
637
|
}
|
|
638
|
+
if (err instanceof UnsupportedResponseFormatError) {
|
|
639
|
+
return c.json({
|
|
640
|
+
error: {
|
|
641
|
+
message: err.message,
|
|
642
|
+
type: 'invalid_request_error',
|
|
643
|
+
param: 'response_format',
|
|
644
|
+
code: err.code,
|
|
645
|
+
},
|
|
646
|
+
}, 400);
|
|
647
|
+
}
|
|
633
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
|
+
}
|
|
634
656
|
if (message.includes('messages must') ||
|
|
635
657
|
message.includes('empty') ||
|
|
636
658
|
message.includes('cannot be an assistant turn')) {
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
+
import { type MemoryProfile } from '../../system/memory.js';
|
|
2
3
|
import type { InferenceContext as ServiceContext } from '../inference-context.js';
|
|
3
4
|
interface V1ModelsRouteOptions {
|
|
4
5
|
/** List only on-device engines plus downloadable on-device catalog entries. */
|
|
5
6
|
localOnly?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* This machine's memory, which downloadable entries are rated against.
|
|
9
|
+
* Defaults to the cached probe `/api/system/memory` reads.
|
|
10
|
+
*/
|
|
11
|
+
memoryProfile?: () => Promise<MemoryProfile>;
|
|
6
12
|
}
|
|
7
13
|
export declare function v1ModelsRoutes(ctx: ServiceContext, options?: V1ModelsRouteOptions): Hono;
|
|
8
14
|
export {};
|