@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "package": "@bendyline/gilde",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "kinds": [
5
5
  "chat-models",
6
6
  "image-models",
@@ -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` = auto-resolve; `null` = explicitly none. */
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;
@@ -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, windowsHeadlessSpawnOptions, } from '@bendyline/gezel/native';
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 `gh`
545
- // login, and requiring one would make on-device engines unreachable for
546
- // every consumer outside this repo.
547
- const token = opts.token !== undefined ? opts.token : await resolveGitHubToken();
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, token);
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, token);
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: makeAuthedFetch(baseFetch, token),
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
- async function resolveGitHubToken() {
769
- if (process.env.GEZEL_GITHUB_TOKEN)
770
- return process.env.GEZEL_GITHUB_TOKEN;
771
- if (process.env.GITHUB_TOKEN)
772
- return process.env.GITHUB_TOKEN;
773
- try {
774
- const { execFileSync } = await import('node:child_process');
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
- function makeAuthedFetch(base, token) {
789
- return ((input, init) => base(input, {
790
- ...init,
791
- headers: {
792
- ...init?.headers,
793
- Accept: 'application/octet-stream',
794
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
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, token) {
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 fetchImpl(url, {
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 — or no access while private)' : ''}`);
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, token) {
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 fetchImpl(assetApiUrl, {
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
- await this.cacheAdapter.flushAll();
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
- catch {
634
- // Best-effort persistence — failure here just means the next
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.5",
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.10",
77
- "@bendyline/gezel-catalog": "1.2.10",
78
- "@bendyline/gezel-client": "1.2.10",
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",