@bendyline/gezel-inference 1.0.0 → 1.0.1
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-manifest.d.ts +8 -0
- package/dist/engines/native-manifest.js +30 -0
- 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/routes/engines.js +9 -3
- package/dist/http/routes/v1-chat.js +21 -8
- package/dist/http/routes/v1-models.d.ts +6 -0
- package/dist/http/routes/v1-models.js +133 -28
- package/dist/http/routes/v1-responses.js +9 -2
- 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/providers/llama-cpp/build-provider.js +33 -8
- package/dist/providers/llama-cpp/cache-adapter.d.ts +12 -0
- package/dist/providers/llama-cpp/cache-adapter.js +34 -0
- package/dist/providers/llama-cpp/gguf-metadata-async.js +7 -0
- package/dist/providers/mlx/provider.js +11 -7
- 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/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 +35 -0
- package/dist/providers/native/capacity-broker.js +69 -4
- package/dist/providers/native/local-engine-runtime.js +7 -2
- 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/memory.js +11 -1
- package/dist/utils/service-worker-entry.d.ts +4 -0
- package/dist/utils/service-worker-entry.js +4 -0
- package/package.json +14 -10
|
@@ -35,6 +35,15 @@ export interface ElectronNativeReuseOptions {
|
|
|
35
35
|
* this false so their parent app is also signature/notarization checked.
|
|
36
36
|
*/
|
|
37
37
|
allowStandaloneMacPayload?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Remember each pinned file that hashed to its pin, with its fingerprint
|
|
40
|
+
* (see file-fingerprint.ts in core), so a later call re-hashes only files
|
|
41
|
+
* whose metadata moved: `nativePayloadVerificationPath(home)`. Everything
|
|
42
|
+
* else still runs on every call, including the listing that rejects a
|
|
43
|
+
* loadable file the release does not pin. Keep it somewhere no easier to
|
|
44
|
+
* write than the payload itself: whoever can edit it can vouch for any bytes.
|
|
45
|
+
*/
|
|
46
|
+
cacheFile?: string;
|
|
38
47
|
}
|
|
39
48
|
/**
|
|
40
49
|
* Adopt Electron's extracted native payload, or an explicitly allowed
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
3
|
-
import { lstat, readdir, readlink } from 'node:fs/promises';
|
|
4
|
-
import { basename, join, posix, relative, resolve, sep } from 'node:path';
|
|
5
|
-
import { resolvePlatformKey } from '@bendyline/gezel/native';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { lstat, mkdir, open, readFile, readdir, readlink, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { basename, dirname, join, posix, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { fileFingerprint, resolvePlatformKey, sameFileFingerprint, } from '@bendyline/gezel/native';
|
|
6
6
|
import pinnedManifestJson from './native-file-manifest.json' with { type: 'json' };
|
|
7
7
|
import { effectiveEngineRelease } from './native-manifest.js';
|
|
8
8
|
import { BENDYLINE_APPLE_TEAM_ID, BENDYLINE_PUBLISHER, verifyCodeSignature } from './signature.js';
|
|
@@ -52,38 +52,89 @@ export async function verifyNativeBinaries(opts) {
|
|
|
52
52
|
reason: `release ${release} has no pinned files for ${baseKey}`,
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
|
+
const cache = opts.cacheFile ? await readHashCache(opts.cacheFile) : null;
|
|
56
|
+
const pinnedCount = platformEntries.reduce((n, [, pin]) => n + Object.keys(pin.files).length, 0);
|
|
55
57
|
const failures = [];
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
58
|
+
try {
|
|
59
|
+
for (const candidate of opts.candidates) {
|
|
60
|
+
const startedAt = Date.now();
|
|
61
|
+
const hashedBefore = cache?.hashed ?? 0;
|
|
62
|
+
try {
|
|
63
|
+
await verifyCandidate({
|
|
64
|
+
root: candidate,
|
|
65
|
+
platform,
|
|
66
|
+
entries: platformEntries,
|
|
67
|
+
verifySignature: opts.verifySignature ?? verifyCodeSignature,
|
|
68
|
+
allowStandaloneMacPayload: opts.allowStandaloneMacPayload === true,
|
|
69
|
+
cache,
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
reused: true,
|
|
73
|
+
nativeBinDir: resolve(candidate),
|
|
74
|
+
// The elapsed time is load-bearing diagnostics, not decoration: this
|
|
75
|
+
// is pre-bind boot work whose cost scales with the payload, and on a
|
|
76
|
+
// CUDA host it is the largest single read the daemon ever does at
|
|
77
|
+
// startup. Without it in the log there is nothing to attribute a slow
|
|
78
|
+
// launch to.
|
|
79
|
+
reason: `verified ${opts.allowStandaloneMacPayload ? 'standalone' : 'Electron'} native release ${release} in ${((Date.now() - startedAt) / 1000).toFixed(1)}s${cache ? ` (${cache.hashed - hashedBefore} of ${pinnedCount} files hashed)` : ''}`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
failures.push(`${candidate}: ${error instanceof Error ? error.message : String(error)}`);
|
|
84
|
+
}
|
|
76
85
|
}
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
return {
|
|
87
|
+
reused: false,
|
|
88
|
+
reason: failures.length > 0
|
|
89
|
+
? failures.join('; ')
|
|
90
|
+
: 'no Electron native payload candidate was installed',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
if (cache)
|
|
95
|
+
await saveHashCache(cache);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function readHashCache(path) {
|
|
99
|
+
const known = new Map();
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
102
|
+
if (isRecord(parsed) && parsed.schema === 1 && isRecord(parsed.files)) {
|
|
103
|
+
for (const [file, record] of Object.entries(parsed.files)) {
|
|
104
|
+
// The fingerprint's shape is checked where it is compared.
|
|
105
|
+
if (isRecord(record) && typeof record.sha256 === 'string') {
|
|
106
|
+
known.set(file, record);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
79
109
|
}
|
|
80
110
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
111
|
+
catch {
|
|
112
|
+
// Missing or unreadable: this call hashes everything and writes it anew.
|
|
113
|
+
}
|
|
114
|
+
return { path, known, examined: new Set(), confirmed: new Map(), hashed: 0 };
|
|
115
|
+
}
|
|
116
|
+
async function saveHashCache(cache) {
|
|
117
|
+
const dropped = [...cache.examined].some((file) => cache.known.has(file) && !cache.confirmed.has(file));
|
|
118
|
+
if (cache.hashed === 0 && !dropped)
|
|
119
|
+
return;
|
|
120
|
+
// Records this call never looked at belong to another candidate, which a
|
|
121
|
+
// dev launch verifies in a separate call against the same file. Dropping
|
|
122
|
+
// them would make the two calls erase each other and hash everything on
|
|
123
|
+
// every launch. They are kept while their files exist.
|
|
124
|
+
const files = {};
|
|
125
|
+
for (const [file, record] of cache.known) {
|
|
126
|
+
if (!cache.examined.has(file) && existsSync(file))
|
|
127
|
+
files[file] = record;
|
|
128
|
+
}
|
|
129
|
+
for (const [file, record] of cache.confirmed)
|
|
130
|
+
files[file] = record;
|
|
131
|
+
try {
|
|
132
|
+
await mkdir(dirname(cache.path), { recursive: true });
|
|
133
|
+
await writeFile(cache.path, `${JSON.stringify({ schema: 1, files }, null, 2)}\n`, 'utf8');
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Only a speedup: the next call hashes what it cannot find here.
|
|
137
|
+
}
|
|
87
138
|
}
|
|
88
139
|
async function verifyCandidate(opts) {
|
|
89
140
|
const root = resolve(opts.root);
|
|
@@ -115,11 +166,13 @@ async function verifyCandidate(opts) {
|
|
|
115
166
|
// Semantics are unchanged: any rejection still fails the candidate, and
|
|
116
167
|
// the first one to fail wins the message.
|
|
117
168
|
// The pin is the whole per-file check; see verifyNativeBinaries.
|
|
169
|
+
const hashBuffers = [];
|
|
118
170
|
await mapWithConcurrency(Object.entries(platformPin.files), 4, async ([relativePath, pin]) => {
|
|
119
171
|
if (!isSafeRelativePath(relativePath)) {
|
|
120
172
|
throw new Error(`unsafe pinned native path: ${relativePath}`);
|
|
121
173
|
}
|
|
122
174
|
const absolute = join(platformDir, ...relativePath.split('/'));
|
|
175
|
+
opts.cache?.examined.add(absolute);
|
|
123
176
|
const info = await lstat(absolute);
|
|
124
177
|
if (!info.isFile() || info.isSymbolicLink()) {
|
|
125
178
|
throw new Error(`pinned file is missing or not regular: ${platformKey}/${relativePath}`);
|
|
@@ -129,10 +182,26 @@ async function verifyCandidate(opts) {
|
|
|
129
182
|
if (info.size !== pin.sizeBytes) {
|
|
130
183
|
throw new Error(`size mismatch: ${platformKey}/${relativePath}`);
|
|
131
184
|
}
|
|
132
|
-
const
|
|
133
|
-
|
|
185
|
+
const want = pin.sha256.toLowerCase();
|
|
186
|
+
// Fingerprinted before hashing, so a file that changes mid-hash is
|
|
187
|
+
// recorded as it was and hashed again next time.
|
|
188
|
+
const fingerprint = opts.cache ? await fileFingerprint(absolute) : null;
|
|
189
|
+
const known = fingerprint ? opts.cache?.known.get(absolute) : undefined;
|
|
190
|
+
if (fingerprint &&
|
|
191
|
+
known?.sha256 === want &&
|
|
192
|
+
sameFileFingerprint(known.fingerprint, fingerprint)) {
|
|
193
|
+
opts.cache?.confirmed.set(absolute, known);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const actualSha = await sha256File(absolute, hashBuffers);
|
|
197
|
+
if (actualSha !== want) {
|
|
134
198
|
throw new Error(`sha256 mismatch: ${platformKey}/${relativePath}`);
|
|
135
199
|
}
|
|
200
|
+
if (opts.cache) {
|
|
201
|
+
opts.cache.hashed += 1;
|
|
202
|
+
if (fingerprint)
|
|
203
|
+
opts.cache.confirmed.set(absolute, { sha256: want, fingerprint });
|
|
204
|
+
}
|
|
136
205
|
});
|
|
137
206
|
for (const [relativePath, target] of Object.entries(platformPin.symlinks)) {
|
|
138
207
|
if (!isSafeRelativePath(relativePath)) {
|
|
@@ -203,18 +272,30 @@ async function listNativeEntries(root, platform) {
|
|
|
203
272
|
}
|
|
204
273
|
/**
|
|
205
274
|
* Run `worker` over `items` with at most `limit` in flight, rejecting with the
|
|
206
|
-
* first failure. Workers already in flight are allowed to settle
|
|
207
|
-
*
|
|
275
|
+
* first failure. Workers already in flight are allowed to settle, and only
|
|
276
|
+
* record files that really did match, so an abandoned hash is inert. No new
|
|
277
|
+
* item starts after a failure: without that, the other workers went on hashing
|
|
278
|
+
* the rest of a rejected payload (a stale 1.6 GB dev checkout) in the
|
|
279
|
+
* background while the next candidate was being verified.
|
|
208
280
|
*/
|
|
209
281
|
async function mapWithConcurrency(items, limit, worker) {
|
|
210
282
|
let next = 0;
|
|
283
|
+
let failed = false;
|
|
211
284
|
const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
212
285
|
for (;;) {
|
|
286
|
+
if (failed)
|
|
287
|
+
return;
|
|
213
288
|
const index = next++;
|
|
214
289
|
const item = items[index];
|
|
215
290
|
if (index >= items.length || item === undefined)
|
|
216
291
|
return;
|
|
217
|
-
|
|
292
|
+
try {
|
|
293
|
+
await worker(item);
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
failed = true;
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
218
299
|
}
|
|
219
300
|
});
|
|
220
301
|
await Promise.all(runners);
|
|
@@ -286,15 +367,35 @@ function enclosingMacApp(path) {
|
|
|
286
367
|
// daemon event loop (a 64 KB stream yields ~100k times and queues behind other
|
|
287
368
|
// work). Engines is a lower layer than models, so this mirrors models'
|
|
288
369
|
// MODEL_HASH_READ_BUFFER_BYTES rather than importing it.
|
|
370
|
+
//
|
|
371
|
+
// The buffer is reused, never one per chunk. A 16 MB `highWaterMark` stream
|
|
372
|
+
// allocates a fresh 16 MB buffer for every read, and in Electron's main process
|
|
373
|
+
// (where the dev supervisor runs this, and the embedded service after it) each
|
|
374
|
+
// allocation cost ~44 ms and left V8 in a ~20 s garbage-collection storm once
|
|
375
|
+
// the 1.7 GB Windows CUDA payload was done: the duckdb and MXC provisioning
|
|
376
|
+
// after it took 4.5 s and 13 s on the splash, against ~50 ms each in isolation.
|
|
377
|
+
// Reusing the buffer hashed the same payload in 1.1 s with no storm.
|
|
289
378
|
const HASH_READ_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
290
|
-
function sha256File(path) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
379
|
+
async function sha256File(path, buffers) {
|
|
380
|
+
const buffer = buffers.pop() ?? Buffer.allocUnsafe(HASH_READ_BUFFER_BYTES);
|
|
381
|
+
try {
|
|
382
|
+
const handle = await open(path, 'r');
|
|
383
|
+
try {
|
|
384
|
+
const hash = createHash('sha256');
|
|
385
|
+
for (;;) {
|
|
386
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
|
|
387
|
+
if (bytesRead === 0)
|
|
388
|
+
return hash.digest('hex');
|
|
389
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
finally {
|
|
393
|
+
await handle.close();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
finally {
|
|
397
|
+
buffers.push(buffer);
|
|
398
|
+
}
|
|
298
399
|
}
|
|
299
400
|
/** Legacy adoption API. New embedding hosts should use the side-effect-free verifier. */
|
|
300
401
|
export async function reuseVerifiedElectronNativeBinaries(opts) {
|
|
@@ -45,12 +45,20 @@ export declare const NATIVE_ENGINE_RELEASE = "0.1.49";
|
|
|
45
45
|
export declare const SHA256SUMS_DIGEST = "51e9d1fcd18174967fed451d11a7401035076330dfedadd6a0ce396ab636cff7";
|
|
46
46
|
/** Exact SHA256 values for every engine archive published by native-v0.1.49. */
|
|
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. */
|
|
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
|
/**
|
|
@@ -62,6 +62,24 @@ export const NATIVE_ENGINE_ARCHIVE_SHA256 = Object.freeze({
|
|
|
62
62
|
'gezel-native-0.1.49-win32-x64-vulkan.zip': '0b7fe1cab3b881ac4a5cc24f08b0206c38edab30940ea79888a70776925a8bfb',
|
|
63
63
|
'gezel-native-0.1.49-win32-x64.zip': 'e8996f82ea358903010e050d6b8b93beec683286af512b11cd8eeefdea50aa6d',
|
|
64
64
|
});
|
|
65
|
+
/** Download size in bytes of every engine archive published by native-v0.1.49. */
|
|
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,
|
|
82
|
+
});
|
|
65
83
|
// END PINNED NATIVE ARCHIVE HASHES
|
|
66
84
|
/**
|
|
67
85
|
* True only when the pinned release's standalone macOS archives were
|
|
@@ -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.
|
|
@@ -4,7 +4,7 @@ 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
9
|
import { EngineBusyError } from '../../providers/native/capacity-broker.js';
|
|
10
10
|
import { invalidateModelsCache } from '../model-inventory-cache.js';
|
|
@@ -39,17 +39,23 @@ export function enginesRoutes(ctx) {
|
|
|
39
39
|
app.get('/binaries/status', (c) => {
|
|
40
40
|
const backendRaw = process.env.GEZEL_LLAMA_SERVER_BACKEND ?? process.env.GEZEL_LLAMA_DETECTED_BACKEND;
|
|
41
41
|
const llamaBackend = ['cuda', 'vulkan', 'metal', 'cpu'].find((value) => value === backendRaw);
|
|
42
|
+
const platformKey = resolvePlatformKey();
|
|
42
43
|
return c.json({
|
|
43
44
|
release: effectiveEngineRelease(),
|
|
44
45
|
pinned: isEnginePinned(),
|
|
45
|
-
platformKey
|
|
46
|
+
platformKey,
|
|
46
47
|
...(llamaBackend ? { llamaBackend } : {}),
|
|
47
48
|
engines: KNOWN_ENGINES.map((name) => {
|
|
48
49
|
const path = process.env[ENGINE_ENV_VAR[name]];
|
|
50
|
+
const installed = !!path && existsSync(path);
|
|
51
|
+
const downloadBytes = !installed && name === 'llama-server' && platformKey && llamaBackend
|
|
52
|
+
? pinnedEngineArchiveBytes(platformKey, llamaBackend)
|
|
53
|
+
: undefined;
|
|
49
54
|
return {
|
|
50
55
|
name,
|
|
51
|
-
installed
|
|
56
|
+
installed,
|
|
52
57
|
...(path ? { path } : {}),
|
|
58
|
+
...(downloadBytes ? { downloadBytes } : {}),
|
|
53
59
|
};
|
|
54
60
|
}),
|
|
55
61
|
});
|
|
@@ -3,12 +3,13 @@ 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';
|
|
12
13
|
import { overlayOpenAiRequestTuning } from '../openai-compat/request-tuning.js';
|
|
13
14
|
import { createStreamingDiagnostics, snapshotStreamingDiagnostics, } from '../openai-compat/streaming-telemetry.js';
|
|
14
15
|
import { runNonStreaming, runStreaming } from '../openai-compat/streaming.js';
|
|
@@ -188,7 +189,11 @@ export function v1ChatRoutes(ctx, opts = {}) {
|
|
|
188
189
|
}
|
|
189
190
|
else {
|
|
190
191
|
const modelTarget = resolveModelTarget(parsed.model);
|
|
191
|
-
|
|
192
|
+
const pinnedTarget = modelTarget ? null : await resolvePinnedModelTarget(ctx, parsed.model);
|
|
193
|
+
if (pinnedTarget) {
|
|
194
|
+
target = pinnedTarget;
|
|
195
|
+
}
|
|
196
|
+
else if (!modelTarget) {
|
|
192
197
|
// Unknown model string (e.g. a client's hardcoded "gpt-4o").
|
|
193
198
|
// Route through the configured fallback gezel, or the Meester
|
|
194
199
|
// when no explicit override is set. Persona + tuning apply
|
|
@@ -199,7 +204,7 @@ export function v1ChatRoutes(ctx, opts = {}) {
|
|
|
199
204
|
if (!fallbackGezelId) {
|
|
200
205
|
return c.json({
|
|
201
206
|
error: {
|
|
202
|
-
message: `Unknown model "${parsed.model}", and no gezel is available to answer it.`,
|
|
207
|
+
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
208
|
type: 'invalid_request_error',
|
|
204
209
|
code: 'gezel_not_found',
|
|
205
210
|
},
|
|
@@ -534,11 +539,9 @@ export function v1ChatRoutes(ctx, opts = {}) {
|
|
|
534
539
|
await stream
|
|
535
540
|
.writeSSE({
|
|
536
541
|
data: JSON.stringify({
|
|
537
|
-
error:
|
|
538
|
-
message,
|
|
539
|
-
type: 'server_error',
|
|
540
|
-
code: 'provider_error',
|
|
541
|
-
},
|
|
542
|
+
error: err instanceof UnsupportedResponseFormatError
|
|
543
|
+
? { message, type: 'invalid_request_error', code: err.code }
|
|
544
|
+
: { message, type: 'server_error', code: 'provider_error' },
|
|
542
545
|
}),
|
|
543
546
|
})
|
|
544
547
|
.catch((sseErr) => {
|
|
@@ -630,6 +633,16 @@ export function v1ChatRoutes(ctx, opts = {}) {
|
|
|
630
633
|
},
|
|
631
634
|
}, 400);
|
|
632
635
|
}
|
|
636
|
+
if (err instanceof UnsupportedResponseFormatError) {
|
|
637
|
+
return c.json({
|
|
638
|
+
error: {
|
|
639
|
+
message: err.message,
|
|
640
|
+
type: 'invalid_request_error',
|
|
641
|
+
param: 'response_format',
|
|
642
|
+
code: err.code,
|
|
643
|
+
},
|
|
644
|
+
}, 400);
|
|
645
|
+
}
|
|
633
646
|
const message = err instanceof Error ? err.message : String(err);
|
|
634
647
|
if (message.includes('messages must') ||
|
|
635
648
|
message.includes('empty') ||
|
|
@@ -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 {};
|