@bendyline/gezel-inference 1.0.5 → 1.0.6
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.
|
@@ -32,7 +32,7 @@ export interface ResolveEngineOptions {
|
|
|
32
32
|
signaturePolicy?: SignaturePolicy;
|
|
33
33
|
fetchImpl?: typeof fetch;
|
|
34
34
|
signal?: AbortSignal;
|
|
35
|
-
/** Pre-resolved token: `undefined` =
|
|
35
|
+
/** Pre-resolved token: `undefined` = from the environment; `null` = explicitly none. */
|
|
36
36
|
token?: string | null;
|
|
37
37
|
/** Override the source-pinned SHA256SUMS digest (tests). */
|
|
38
38
|
expectedSha256sumsDigest?: string;
|
package/dist/engines/resolver.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createReadStream, existsSync, readdirSync } from 'node:fs';
|
|
|
3
3
|
import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { HttpStatusError, createLogger, nowIso, resolveDistributionProfile, retryTransient, } from '@bendyline/gezel';
|
|
6
|
-
import { DUCKDB_APPLE_TEAM_ID, DUCKDB_ARCHIVE_SHA256, DUCKDB_ASSET, DUCKDB_BINARY_SHA256, DUCKDB_VERSION, DUCKDB_WINDOWS_PUBLISHER, MXC_EXECUTOR_NAME, MXC_EXECUTOR_SHA256, MXC_LICENSE_SHA256, MXC_TARBALL_INTEGRITY, MXC_TARBALL_LICENSE_ENTRY, MXC_TARBALL_SHA256, MXC_TARBALL_URL, MXC_VERIFIED_SENTINEL, MXC_VERSION, MXC_WINDOWS_PUBLISHER, duckdbAssetUrl, duckdbBinaryName, duckdbInstallDir, duckdbPlatformKey, mxcArch, mxcInstallDir, mxcTarballEntry, resolvePlatformKey,
|
|
6
|
+
import { DUCKDB_APPLE_TEAM_ID, DUCKDB_ARCHIVE_SHA256, DUCKDB_ASSET, DUCKDB_BINARY_SHA256, DUCKDB_VERSION, DUCKDB_WINDOWS_PUBLISHER, MXC_EXECUTOR_NAME, MXC_EXECUTOR_SHA256, MXC_LICENSE_SHA256, MXC_TARBALL_INTEGRITY, MXC_TARBALL_LICENSE_ENTRY, MXC_TARBALL_SHA256, MXC_TARBALL_URL, MXC_VERIFIED_SENTINEL, MXC_VERSION, MXC_WINDOWS_PUBLISHER, duckdbAssetUrl, duckdbBinaryName, duckdbInstallDir, duckdbPlatformKey, mxcArch, mxcInstallDir, mxcTarballEntry, resolvePlatformKey, } from '@bendyline/gezel/native';
|
|
7
7
|
import * as tar from 'tar';
|
|
8
8
|
import { downloadWithRetry, friendlyErrors, } from '../utils/download-with-retry.js';
|
|
9
9
|
import { extractZip } from '../utils/extract-zip.js';
|
|
@@ -541,14 +541,16 @@ export async function* resolveEngine(opts) {
|
|
|
541
541
|
// `bendyline/gezel` is public, so release metadata and assets are readable
|
|
542
542
|
// anonymously. A token stays OPTIONAL and is used when present purely to
|
|
543
543
|
// lift GitHub's 60-request/hour unauthenticated API rate limit. Do not
|
|
544
|
-
// reinstate a hard gate here: users who install from npm have no
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
const
|
|
544
|
+
// reinstate a hard gate here: users who install from npm have no token,
|
|
545
|
+
// and requiring one would make on-device engines unreachable for every
|
|
546
|
+
// consumer outside this repo.
|
|
547
|
+
const auth = {
|
|
548
|
+
token: opts.token !== undefined ? opts.token : resolveGitHubToken(),
|
|
549
|
+
};
|
|
548
550
|
const baseFetch = opts.fetchImpl ?? globalThis.fetch;
|
|
549
551
|
const tag = `native-v${version}`;
|
|
550
552
|
// ── 1. Resolve release + assets ──
|
|
551
|
-
const release = await ghJson(`${apiBase}/repos/${REPO}/releases/tags/${tag}`, baseFetch,
|
|
553
|
+
const release = await ghJson(`${apiBase}/repos/${REPO}/releases/tags/${tag}`, baseFetch, auth);
|
|
552
554
|
const archiveAsset = release.assets?.find((a) => a.name === archiveName);
|
|
553
555
|
const sumsAsset = release.assets?.find((a) => a.name === 'SHA256SUMS');
|
|
554
556
|
if (!archiveAsset) {
|
|
@@ -558,7 +560,7 @@ export async function* resolveEngine(opts) {
|
|
|
558
560
|
yield* fail(`release ${tag} is missing its SHA256SUMS manifest — refusing to install unverified`);
|
|
559
561
|
}
|
|
560
562
|
// ── 2. SHA256SUMS: verify own digest against the source pin, then parse ──
|
|
561
|
-
const sumsBytes = await ghBytes(sumsAsset.url, baseFetch,
|
|
563
|
+
const sumsBytes = await ghBytes(sumsAsset.url, baseFetch, auth);
|
|
562
564
|
const sumsDigest = createHash('sha256').update(sumsBytes).digest('hex');
|
|
563
565
|
if (!unpinned && sumsDigest !== expectedDigest) {
|
|
564
566
|
yield* fail(`SHA256SUMS digest mismatch for ${tag}: this build pins ${expectedDigest.slice(0, 16)}… but the release served ${sumsDigest.slice(0, 16)}…. Refusing — the release may have been tampered with or re-cut.`);
|
|
@@ -584,7 +586,7 @@ export async function* resolveEngine(opts) {
|
|
|
584
586
|
url: archiveAsset.url,
|
|
585
587
|
destPath: archivePath,
|
|
586
588
|
approxSizeBytes: archiveAsset.size ?? 0,
|
|
587
|
-
fetchImpl:
|
|
589
|
+
fetchImpl: gitHubFetch(baseFetch, auth, 'application/octet-stream'),
|
|
588
590
|
...(opts.signal ? { signal: opts.signal } : {}),
|
|
589
591
|
});
|
|
590
592
|
let outcome;
|
|
@@ -765,35 +767,37 @@ function stampEnv(engine, binPath, variant, versionDir) {
|
|
|
765
767
|
process.env.GEZEL_SQLITE_VEC_PATH = sqliteVec;
|
|
766
768
|
}
|
|
767
769
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
// Named host: a bare `gh auth token` answers for gh's default host, which
|
|
776
|
-
// can be a GitHub Enterprise server whose token must not go to github.com.
|
|
777
|
-
const out = execFileSync('gh', ['auth', 'token', '--hostname', 'github.com'], {
|
|
778
|
-
encoding: 'utf8',
|
|
779
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
780
|
-
...windowsHeadlessSpawnOptions(),
|
|
781
|
-
});
|
|
782
|
-
return out.trim() || null;
|
|
783
|
-
}
|
|
784
|
-
catch {
|
|
785
|
-
return null;
|
|
786
|
-
}
|
|
770
|
+
/**
|
|
771
|
+
* An optional token, from the environment only. This module runs inside apps
|
|
772
|
+
* that embed Gezel, so it never borrows the user's GitHub CLI login: `gh` is
|
|
773
|
+
* not its credential to use, and a stale one used to fail public downloads.
|
|
774
|
+
*/
|
|
775
|
+
function resolveGitHubToken() {
|
|
776
|
+
return process.env.GEZEL_GITHUB_TOKEN || process.env.GITHUB_TOKEN || null;
|
|
787
777
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
778
|
+
/**
|
|
779
|
+
* Fetch from GitHub with the optional token. The release is public, so a token
|
|
780
|
+
* GitHub rejects (expired, revoked) is dropped for the rest of the resolve and
|
|
781
|
+
* the request repeats anonymously instead of failing the install.
|
|
782
|
+
*/
|
|
783
|
+
function gitHubFetch(base, auth, accept) {
|
|
784
|
+
return (async (input, init) => {
|
|
785
|
+
const send = () => base(input, {
|
|
786
|
+
...init,
|
|
787
|
+
headers: {
|
|
788
|
+
...init?.headers,
|
|
789
|
+
Accept: accept,
|
|
790
|
+
...(auth.token ? { Authorization: `Bearer ${auth.token}` } : {}),
|
|
791
|
+
},
|
|
792
|
+
});
|
|
793
|
+
const res = await send();
|
|
794
|
+
if (res.status !== 401 || !auth.token)
|
|
795
|
+
return res;
|
|
796
|
+
await res.body?.cancel().catch(() => { });
|
|
797
|
+
log.warn('[engine-resolver] GitHub rejected the token from GEZEL_GITHUB_TOKEN / GITHUB_TOKEN (401); continuing anonymously');
|
|
798
|
+
auth.token = null;
|
|
799
|
+
return send();
|
|
800
|
+
});
|
|
797
801
|
}
|
|
798
802
|
/**
|
|
799
803
|
* The archive itself goes through `downloadWithRetry`, but the two small
|
|
@@ -808,31 +812,23 @@ function ghRetryLog(what) {
|
|
|
808
812
|
log.warn(`[engine-resolver] ${what} failed (${info.reason}); retrying in ${info.delayMs}ms (attempt ${info.attempt}/${info.maxAttempts})`);
|
|
809
813
|
};
|
|
810
814
|
}
|
|
811
|
-
async function ghJson(url, fetchImpl,
|
|
815
|
+
async function ghJson(url, fetchImpl, auth) {
|
|
816
|
+
const get = gitHubFetch(fetchImpl, auth, 'application/vnd.github+json');
|
|
812
817
|
return retryTransient(async () => {
|
|
813
|
-
const res = await
|
|
814
|
-
headers: {
|
|
815
|
-
Accept: 'application/vnd.github+json',
|
|
816
|
-
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
817
|
-
},
|
|
818
|
-
});
|
|
818
|
+
const res = await get(url);
|
|
819
819
|
if (!res.ok) {
|
|
820
820
|
await res.body?.cancel().catch(() => { });
|
|
821
821
|
// A transient status is thrown as HttpStatusError so retryTransient
|
|
822
822
|
// takes another pass; 4xx becomes the terminal EngineUnavailableError.
|
|
823
|
-
throw new HttpStatusError(res.status, `GitHub API returned ${res.status} for ${url}${res.status === 404 ? ' (release not found
|
|
823
|
+
throw new HttpStatusError(res.status, `GitHub API returned ${res.status} for ${url}${res.status === 404 ? ' (release not found)' : ''}`);
|
|
824
824
|
}
|
|
825
825
|
return (await res.json());
|
|
826
826
|
}, { attempts: GH_METADATA_ATTEMPTS, onRetry: ghRetryLog('GitHub release lookup') }).catch(rethrowAsEngineUnavailable);
|
|
827
827
|
}
|
|
828
|
-
async function ghBytes(assetApiUrl, fetchImpl,
|
|
828
|
+
async function ghBytes(assetApiUrl, fetchImpl, auth) {
|
|
829
|
+
const get = gitHubFetch(fetchImpl, auth, 'application/octet-stream');
|
|
829
830
|
return retryTransient(async () => {
|
|
830
|
-
const res = await
|
|
831
|
-
headers: {
|
|
832
|
-
Accept: 'application/octet-stream',
|
|
833
|
-
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
834
|
-
},
|
|
835
|
-
});
|
|
831
|
+
const res = await get(assetApiUrl);
|
|
836
832
|
if (!res.ok) {
|
|
837
833
|
await res.body?.cancel().catch(() => { });
|
|
838
834
|
throw new HttpStatusError(res.status, `asset download returned ${res.status} for ${assetApiUrl}`);
|
|
@@ -32,6 +32,13 @@ import { classifyStartupLine } from './stdout-parser.js';
|
|
|
32
32
|
export * from '@bendyline/gezel/local-loop';
|
|
33
33
|
export { isFileRepairTurn as isScenarioFileRepairTurn } from '@bendyline/gezel/local-loop';
|
|
34
34
|
const log = createLogger('llama-cpp');
|
|
35
|
+
/**
|
|
36
|
+
* How long shutdown waits for the slot caches to reach disk. The app gives
|
|
37
|
+
* the whole daemon 15 s to quit, and a save the engine never answers must
|
|
38
|
+
* not spend all of it: an unsaved or half-written slot only means a fresh
|
|
39
|
+
* prefill next time, because a restore that fails falls back to one.
|
|
40
|
+
*/
|
|
41
|
+
const SHUTDOWN_FLUSH_BUDGET_MS = 8_000;
|
|
35
42
|
export class LlamaCppProvider {
|
|
36
43
|
name = 'llama-cpp';
|
|
37
44
|
queue;
|
|
@@ -627,12 +634,21 @@ export class LlamaCppProvider {
|
|
|
627
634
|
// llama-server exits the slot save endpoint is gone. flushAll is
|
|
628
635
|
// best-effort; failures during shutdown shouldn't block the stop.
|
|
629
636
|
if (this.cacheAdapter) {
|
|
637
|
+
let timer;
|
|
638
|
+
const budget = new Promise((resolve) => {
|
|
639
|
+
timer = setTimeout(() => resolve('timeout'), SHUTDOWN_FLUSH_BUDGET_MS);
|
|
640
|
+
timer.unref?.();
|
|
641
|
+
});
|
|
630
642
|
try {
|
|
631
|
-
|
|
643
|
+
// Best-effort persistence: a failure only means the next boot pays
|
|
644
|
+
// the prefill cost on resume.
|
|
645
|
+
const outcome = await Promise.race([this.cacheAdapter.flushAll().catch(() => 0), budget]);
|
|
646
|
+
if (outcome === 'timeout') {
|
|
647
|
+
log.warn(`slot cache save still running after ${SHUTDOWN_FLUSH_BUDGET_MS}ms at shutdown; stopping the engine`);
|
|
648
|
+
}
|
|
632
649
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
// boot pays the prefill cost on resume, same as before.
|
|
650
|
+
finally {
|
|
651
|
+
clearTimeout(timer);
|
|
636
652
|
}
|
|
637
653
|
}
|
|
638
654
|
await this.supervisor?.stop();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bendyline/gezel-inference",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Gezel's on-device inference runtime: native engines, local model management, embeddings, knowledge search, and OpenAI-compatible routes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -73,9 +73,9 @@
|
|
|
73
73
|
"!dist/**/__pycache__"
|
|
74
74
|
],
|
|
75
75
|
"dependencies": {
|
|
76
|
-
"@bendyline/gezel": "1.2.
|
|
77
|
-
"@bendyline/gezel-catalog": "1.2.
|
|
78
|
-
"@bendyline/gezel-client": "1.2.
|
|
76
|
+
"@bendyline/gezel": "1.2.11",
|
|
77
|
+
"@bendyline/gezel-catalog": "1.2.11",
|
|
78
|
+
"@bendyline/gezel-client": "1.2.11",
|
|
79
79
|
"@bendyline/gezel-knowledge": "1.2.9",
|
|
80
80
|
"hono": "4.13.9",
|
|
81
81
|
"tar": "7.5.21",
|