@bendyline/gezel-service 1.1.1 → 1.1.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/dist/NOTICE.md +3 -3
- package/dist/bin/gezeld.js +8471 -5311
- package/dist/handboek-content/technical/building-connected-apps-with-gezel-app-sdk.md +78 -0
- package/dist/handboek-content/whats-new/{1.26250.md → 1.26251.md} +5 -5
- package/dist/index-store/static-index-worker.js +160 -51
- package/dist/index.d.ts +416 -71
- package/dist/index.js +8465 -5305
- package/dist/memory/embed-worker.js +132 -4
- package/dist/memory/image-embed-worker.js +14 -5
- package/dist/providers/mlx/python/cache_persist.py +52 -8
- package/dist/providers/mlx/python/gezel_mlx_server.py +44 -1
- package/dist/providers/mlx/python/qwen4_ple.py +159 -0
- package/dist/providers/mlx/python/spec_decode.py +73 -31
- package/dist/providers/mlx/python/tool_call_stream.py +10 -2
- package/dist/providers/mlx/python/tool_grammar.py +16 -0
- package/dist/ui/assets/{AmbientDashboardCard-DcVHxEdC.js → AmbientDashboardCard-_xnAQGnk.js} +1 -1
- package/dist/ui/assets/{AudioEngineSettings-BFM2S1mm.js → AudioEngineSettings-CgXgbX0i.js} +1 -1
- package/dist/ui/assets/{BenchmarksView-84-YeULA.js → BenchmarksView-BlExZpYA.js} +1 -1
- package/dist/ui/assets/CatalogBrowser-DYXUZQ_t.js +1 -0
- package/dist/ui/assets/{ChannelsSettings-7NAVUk4k.js → ChannelsSettings-EH27HcqZ.js} +1 -1
- package/dist/ui/assets/ChatTimelineView-Dn1TNyVn.js +7 -0
- package/dist/ui/assets/{ConnectedAppsPanel-CIgF4Q3R.js → ConnectedAppsPanel-BROry-Y0.js} +1 -1
- package/dist/ui/assets/CraftbookEditor-B2RNJnFf.js +1 -0
- package/dist/ui/assets/{CraftbookScriptEditorView-Ck5tzj3Y.js → CraftbookScriptEditorView-B-vySmHP.js} +1 -1
- package/dist/ui/assets/{CraftbookTabContent-BWg7FhK_.js → CraftbookTabContent-BW_2KC89.js} +1 -1
- package/dist/ui/assets/{CraftbookToolsetSetup-EuFOiUzR.js → CraftbookToolsetSetup-J7rEa835.js} +1 -1
- package/dist/ui/assets/{CraftbooksView-BIzQ1VVn.js → CraftbooksView-BjBUWPGF.js} +1 -1
- package/dist/ui/assets/DiffpackReviewView-CD4qJmgT.js +1 -0
- package/dist/ui/assets/DocumentDetail-SYozruTJ.js +1 -0
- package/dist/ui/assets/DocumentsView-DE7eL49Z.js +1 -0
- package/dist/ui/assets/{Ds4Settings-9soZCSeO.js → Ds4Settings-DGzc7JcD.js} +1 -1
- package/dist/ui/assets/{EmbeddedChat-Cce7nxXT.js → EmbeddedChat-D92TaJQ8.js} +1 -1
- package/dist/ui/assets/{FaceRecognitionCard-BVZyOwB8.js → FaceRecognitionCard-CB1Dt9X4.js} +1 -1
- package/dist/ui/assets/FileMapView-kVBG22FU.js +1 -0
- package/dist/ui/assets/FoldersSettings-BWFQU5B4.js +1 -0
- package/dist/ui/assets/GezelDetail-Br4v42Nv.js +1 -0
- package/dist/ui/assets/{GezelJsonEditor-DXXt165-.js → GezelJsonEditor-B9XEiW5Y.js} +1 -1
- package/dist/ui/assets/GezellenView-DheEBAy4.js +1 -0
- package/dist/ui/assets/{GildeUpdatesCard-CtY8mUTA.js → GildeUpdatesCard-BeIYv09f.js} +1 -1
- package/dist/ui/assets/{GithubSignInChip-DT3l6E0Q.js → GithubSignInChip-eEGCe9ar.js} +1 -1
- package/dist/ui/assets/{HandboekMediaProvider-ByJ4NAzz.js → HandboekMediaProvider-D0zcbbpf.js} +1 -1
- package/dist/ui/assets/HandboekView-p0BZT1rH.js +1 -0
- package/dist/ui/assets/{HistoryView-CkinzFEC.js → HistoryView-CKWcWW-Y.js} +1 -1
- package/dist/ui/assets/{HomeView-DNBrQnwy.js → HomeView-b_TT5oV3.js} +1 -1
- package/dist/ui/assets/{ImageEngineSettings-BfM9HSd_.js → ImageEngineSettings-CJfk4OJ4.js} +1 -1
- package/dist/ui/assets/{ImageRecognitionSettings-CaSQu5Br.js → ImageRecognitionSettings-DzbvZk5S.js} +1 -1
- package/dist/ui/assets/{IncompleteDownloads-C36lTCMG.js → IncompleteDownloads-C8_XyP8O.js} +1 -1
- package/dist/ui/assets/{KnowledgeCatalogsCard-CK9bqdf6.js → KnowledgeCatalogsCard-CHopG_j-.js} +1 -1
- package/dist/ui/assets/KnowledgeView-CXNKZUJq.js +1 -0
- package/dist/ui/assets/{LlamaCppSettings-CdNMHBbp.js → LlamaCppSettings-ohcM7kes.js} +1 -1
- package/dist/ui/assets/{MarkdownField-Dugmfr_e.js → MarkdownField-BX0qsaFq.js} +1 -1
- package/dist/ui/assets/{MemoriesTree-CgFqUNEr.js → MemoriesTree-BPj5Rod8.js} +1 -1
- package/dist/ui/assets/{MicrophoneSettings-DzDe9y1q.js → MicrophoneSettings-CWjeYnfI.js} +1 -1
- package/dist/ui/assets/MlxSettings-Dq6dRpcl.js +1 -0
- package/dist/ui/assets/{ModelPicker-BVkJhNAH.js → ModelPicker-0Ue-eEKj.js} +1 -1
- package/dist/ui/assets/NewPathDialog-BhAZm9dp.js +1 -0
- package/dist/ui/assets/{NewScriptDialog-TEAd0KLl.js → NewScriptDialog-DuryIe50.js} +1 -1
- package/dist/ui/assets/NewTaskDialog-BX24flMY.js +2 -0
- package/dist/ui/assets/{OllamaSettings-CPy2vxyZ.js → OllamaSettings-Boj8Nd1G.js} +1 -1
- package/dist/ui/assets/ProjectChat-Dhw_M5bq.js +5 -0
- package/dist/ui/assets/ProjectConnectionsTab-BwpMQNrg.js +1 -0
- package/dist/ui/assets/{ProjectGitStatusBar-BgHZ2ijN.js → ProjectGitStatusBar-BORGYx5l.js} +1 -1
- package/dist/ui/assets/{ProjectGithubView-BV2uTNU0.js → ProjectGithubView-CTP1BOMv.js} +1 -1
- package/dist/ui/assets/{ProjectMailTab-DLtJdXSL.js → ProjectMailTab-qFmAeV3M.js} +1 -1
- package/dist/ui/assets/{ProjectOverviewView-ZMs9nL5N.js → ProjectOverviewView-Cq1FsoaT.js} +1 -1
- package/dist/ui/assets/{ProjectTimeline-CTPcn4Kc.js → ProjectTimeline-Cqae2Z92.js} +1 -1
- package/dist/ui/assets/ProjectsView-D0uJjUB-.js +7 -0
- package/dist/ui/assets/{RecommendedBadge-iUoisQBi.js → RecommendedBadge-CN1jEmSn.js} +1 -1
- package/dist/ui/assets/{RemoteServersPanel-CTVJ0IRh.js → RemoteServersPanel-CMakvCCM.js} +1 -1
- package/dist/ui/assets/{ScriptEditorTabs-CCDFZjdC.js → ScriptEditorTabs-st6dJePd.js} +2 -2
- package/dist/ui/assets/{ScriptEditorView-CergLqKj.js → ScriptEditorView-BkySh57_.js} +2 -2
- package/dist/ui/assets/{ScriptsView-v7ckdhDi.js → ScriptsView-7uPKwjDB.js} +1 -1
- package/dist/ui/assets/{SecurityComplianceSettings-Bd0-66Gs.js → SecurityComplianceSettings-Tsv_pXfy.js} +1 -1
- package/dist/ui/assets/SessionSwitcher-D0fSeeHO.js +1 -0
- package/dist/ui/assets/SettingsView-cli_z8ZW.js +3 -0
- package/dist/ui/assets/{SquisqIntegration-C5l_OPvD.js → SquisqIntegration-BTZ1kBlg.js} +1 -1
- package/dist/ui/assets/{StorageCleanupDialog-3kv9-2mT.js → StorageCleanupDialog-CDBjl8JQ.js} +1 -1
- package/dist/ui/assets/{StorageUsageCard-7sWTrZFa.js → StorageUsageCard-DXY1j0dZ.js} +1 -1
- package/dist/ui/assets/TaskTabContent-C0P681SL.js +1 -0
- package/dist/ui/assets/TasksView-Ddwz10ri.js +1 -0
- package/dist/ui/assets/{TerminalCodeEditor-ChmdzmCz.js → TerminalCodeEditor-BkNQ6QoQ.js} +2 -2
- package/dist/ui/assets/ToolsetsEditor-KVigemfL.js +8 -0
- package/dist/ui/assets/{TransformToolbarButton-gzIxUBH7.js → TransformToolbarButton-8wfJR1bO.js} +2 -2
- package/dist/ui/assets/{UnrecognizedModels-0hptopv6.js → UnrecognizedModels-BzdU1clO.js} +1 -1
- package/dist/ui/assets/{VideoEngineSettings-BdNzCEdR.js → VideoEngineSettings-f79ff57u.js} +1 -1
- package/dist/ui/assets/{WorkspaceIndexPane-DnsOO916.js → WorkspaceIndexPane-fu5V6o1W.js} +1 -1
- package/dist/ui/assets/{api-C0I7spBl.js → api-CmrgM5Rf.js} +1 -1
- package/dist/ui/assets/chat-thread-memory-8MCtb17Q.js +1 -0
- package/dist/ui/assets/{chunk-6ECMNUIP-BvCy0HUI.js → chunk-6ECMNUIP-D5AA6ozp.js} +1 -1
- package/dist/ui/assets/{chunk-A6N6IN3I-ofeM4l8g.js → chunk-A6N6IN3I-BpxihS5H.js} +1 -1
- package/dist/ui/assets/{chunk-ATWVNF7D-CYuW1Yc6.js → chunk-ATWVNF7D-CKIWh7bN.js} +1 -1
- package/dist/ui/assets/{chunk-UBFT2CDQ-FexGDigd.js → chunk-CW24CH7I-BD0FIooX.js} +1 -1
- package/dist/ui/assets/{chunk-DXYWKZ52-zxe8pjH4.js → chunk-DXYWKZ52-DMKPZnpo.js} +1 -1
- package/dist/ui/assets/chunk-IIQYS2YH-BtDJt4Xr.js +1 -0
- package/dist/ui/assets/{chunk-KGWT454C-v5-l6U5m.js → chunk-KGWT454C-DsQOBuLg.js} +1 -1
- package/dist/ui/assets/{chunk-Q77KNJIN-BLuPdsQF.js → chunk-Q77KNJIN-BdWizq9s.js} +1 -1
- package/dist/ui/assets/{chunk-USU6HTKB-B_p-YcDp.js → chunk-USU6HTKB-DyGZplh4.js} +1 -1
- package/dist/ui/assets/{chunk-UXEANVBI-Bt0kX-0Z.js → chunk-UXEANVBI-BUBXvqyE.js} +1 -1
- package/dist/ui/assets/{chunk-O7JILDEF-CqcO1qaI.js → chunk-X7BNDUNI-D14WIvlD.js} +2 -2
- package/dist/ui/assets/{chunk-AE3IUKMM-mZjzzO8Z.js → chunk-ZSU5DJLX-CrjQf-vs.js} +1 -1
- package/dist/ui/assets/{container-BI8hxN1N.js → container-iFj1r6Hk.js} +1 -1
- package/dist/ui/assets/csv-Di411Mv-.js +1 -0
- package/dist/ui/assets/data-Q05qqAp4.js +1 -0
- package/dist/ui/assets/{dist-APSIK7H3.js → dist-1FNFiksd.js} +3 -3
- package/dist/ui/assets/dist-BMZhqfv7.js +2 -0
- package/dist/ui/assets/dist-CE5CbmDb.js +225 -0
- package/dist/ui/assets/doc-BHcGsJZd.js +587 -0
- package/dist/ui/assets/{document-import-DPK4r8Am.js → document-import-B2Ya6sbf.js} +1 -1
- package/dist/ui/assets/{docx-DRLuCTaC.js → docx-B8Pwz5Ih.js} +1 -1
- package/dist/ui/assets/{epub-a0p2Xb1b.js → epub-BQ4g8Q9j.js} +1 -1
- package/dist/ui/assets/{export-options-BqYFyzWx.js → export-options-Dylv69b3.js} +1 -1
- package/dist/ui/assets/file-browser-BLOPfnS0.js +5 -0
- package/dist/ui/assets/{html-BbQNmJ5X.js → html-BnA7qFwq.js} +1 -1
- package/dist/ui/assets/icons-CyM7Cg4E.js +1 -0
- package/dist/ui/assets/index-DW6qHqhU.css +1 -0
- package/dist/ui/assets/index-DivKnJyx.js +36 -0
- package/dist/ui/assets/{mail-link-CWfsP5Aq.js → mail-link-NtlcX5OE.js} +1 -1
- package/dist/ui/assets/{markdown-ryKA_OmY.js → markdown-CLKH_zzl.js} +1 -1
- package/dist/ui/assets/markdown-baseline-0dqv9qbV.js +1 -0
- package/dist/ui/assets/materialize-G4WC5UG3-xq2dcwS8.js +1 -0
- package/dist/ui/assets/{monaco-base-C0VTMpv-.js → monaco-base-BSvdAweA.js} +1 -1
- package/dist/ui/assets/{monaco-setup-CX8q88c5.js → monaco-setup-CT7LZ0o_.js} +1 -1
- package/dist/ui/assets/{new-project-meta-ZmDPGYzC.js → new-project-meta-5Y9Blozr.js} +1 -1
- package/dist/ui/assets/outside-in-Msmhxf_U.js +3 -0
- package/dist/ui/assets/{pdf-BHrygJa5.js → pdf-C4BhW3zg.js} +1 -1
- package/dist/ui/assets/{pending-drafts-27ujtQxX.js → pending-drafts-BD227X9H.js} +1 -1
- package/dist/ui/assets/{poppetje-CfaAvFkE.js → poppetje-DvCJO36y.js} +3 -3
- package/dist/ui/assets/{pptx-nDW64jAN.js → pptx-B6uaM2s7.js} +3 -3
- package/dist/ui/assets/{run-export-86KzLZ7R.js → run-export-TscWwR6y.js} +2 -2
- package/dist/ui/assets/{standalone-source-7QdiNsDW.js → standalone-source-Cw9SF7Zk.js} +1 -1
- package/dist/ui/assets/storage-Cmhlwx70.js +1 -0
- package/dist/ui/assets/styles-BjDaWeMA.css +1 -0
- package/dist/ui/assets/styles-CqO-rmOt.js +554 -0
- package/dist/ui/assets/{terminal-monaco-setup-Bar-sYKd.js → terminal-monaco-setup-me523-j-.js} +1 -1
- package/dist/ui/assets/{transform-CmAXy0Ck.js → transform-DxJCwK68.js} +1 -1
- package/dist/ui/assets/{use-local-model-manager-D1yiurrC.js → use-local-model-manager-8wvk-I-y.js} +1 -1
- package/dist/ui/assets/{useCompactLayout-BDtPQcDh.js → useCompactLayout-cm4E9Vja.js} +0 -0
- package/dist/ui/assets/{useCopilotAvailability-BHOfHrfT.js → useCopilotAvailability-D9hglSH_.js} +1 -1
- package/dist/ui/assets/{useDiffpacks-B0kYhlWQ.js → useDiffpacks-1BN2XJWg.js} +1 -1
- package/dist/ui/assets/{useGitSync-DOt-ODiw.js → useGitSync-7m9YA7h5.js} +1 -1
- package/dist/ui/assets/{useShowWorkInProgressFeatures-BtpredG7.js → useShowWorkInProgressFeatures-D6QOaTUT.js} +1 -1
- package/dist/ui/assets/{useTotalRamBytes-B8NFM27w.js → useTotalRamBytes-Durk383U.js} +1 -1
- package/dist/ui/assets/{xlsx-B_pu1ZvY.js → xlsx-BbHA9bxA.js} +1 -1
- package/dist/ui/index.html +15 -15
- package/package.json +12 -11
- package/dist/ui/assets/CatalogBrowser-7_jHy5Q3.js +0 -1
- package/dist/ui/assets/ChatTimelineView-Cg_kLK_U.js +0 -7
- package/dist/ui/assets/CraftbookEditor-BKAIsnXv.js +0 -1
- package/dist/ui/assets/DiffpackReviewView-B2a3zgE-.js +0 -1
- package/dist/ui/assets/DocumentDetail-GCYhwQqV.js +0 -1
- package/dist/ui/assets/DocumentsView-Cm3RaQ5N.js +0 -1
- package/dist/ui/assets/FileMapView-BKJovLcL.js +0 -1
- package/dist/ui/assets/FoldersSettings-CuJd2JdE.js +0 -1
- package/dist/ui/assets/GezelDetail-D_KIPDhQ.js +0 -1
- package/dist/ui/assets/GezellenView-BOOW1QQU.js +0 -1
- package/dist/ui/assets/HandboekView-gRzJcIbJ.js +0 -1
- package/dist/ui/assets/KnowledgeView-DJ3cJVHA.js +0 -1
- package/dist/ui/assets/MlxSettings-45mBFHbA.js +0 -1
- package/dist/ui/assets/NewTaskDialog-CIXVunZn.js +0 -2
- package/dist/ui/assets/ProjectChat-BiJRweob.js +0 -5
- package/dist/ui/assets/ProjectConnectionsTab-CQgaIzuv.js +0 -1
- package/dist/ui/assets/ProjectsView-BUdR76n6.js +0 -7
- package/dist/ui/assets/SessionSwitcher-BgZqyiRn.js +0 -1
- package/dist/ui/assets/SettingsView-DLhOYiR5.js +0 -3
- package/dist/ui/assets/TaskTabContent-CG2KmfwV.js +0 -1
- package/dist/ui/assets/TasksView-Dy5Ue4zS.js +0 -1
- package/dist/ui/assets/ToolsetsEditor-J9XxQ5Ms.js +0 -8
- package/dist/ui/assets/chat-thread-memory-CI2-giEq.js +0 -1
- package/dist/ui/assets/chunk-IIQYS2YH-DEkyBlHg.js +0 -1
- package/dist/ui/assets/csv-B0D9jJG2.js +0 -1
- package/dist/ui/assets/data-BIL84EaT.js +0 -1
- package/dist/ui/assets/dist-CJqyWK6E.js +0 -2
- package/dist/ui/assets/dist-DOqbRmFj.js +0 -225
- package/dist/ui/assets/doc-C2Ph2FWI.js +0 -584
- package/dist/ui/assets/document-companion-aDThLOwl.js +0 -1
- package/dist/ui/assets/file-browser-DVyWN0LD.js +0 -5
- package/dist/ui/assets/icons-DWaUNQbT.js +0 -1
- package/dist/ui/assets/index-DDu8nyO6.js +0 -34
- package/dist/ui/assets/index-DEDUSiCx.css +0 -1
- package/dist/ui/assets/markdown-baseline-9B52XlX2.js +0 -1
- package/dist/ui/assets/materialize-G4WC5UG3-BlZQe-88.js +0 -1
- package/dist/ui/assets/outside-in-BmIIeMid.js +0 -3
- package/dist/ui/assets/storage-B-vhjyV2.js +0 -1
- package/dist/ui/assets/styles-BvkZd-EV.js +0 -550
- package/dist/ui/assets/styles-IQ4adxWr.css +0 -1
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { parentPort } from "worker_threads";
|
|
5
5
|
|
|
6
6
|
// src/memory/embed-core.ts
|
|
7
|
-
import { createHash } from "crypto";
|
|
7
|
+
import { createHash as createHash2 } from "crypto";
|
|
8
8
|
import { createLogger as createLogger2 } from "@bendyline/gezel";
|
|
9
9
|
import {
|
|
10
10
|
EmbedderUnavailableError,
|
|
@@ -15,12 +15,131 @@ import {
|
|
|
15
15
|
} from "@bendyline/gezel-knowledge";
|
|
16
16
|
|
|
17
17
|
// src/transformers-cache.ts
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
18
|
+
import { createHash } from "crypto";
|
|
19
|
+
import { mkdir, rm, stat, writeFile } from "fs/promises";
|
|
20
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
20
21
|
import { createLogger } from "@bendyline/gezel";
|
|
22
|
+
|
|
23
|
+
// src/fs/safe-paths.ts
|
|
24
|
+
import { realpath } from "fs/promises";
|
|
25
|
+
import { basename, dirname, isAbsolute, join, normalize, resolve, sep } from "path";
|
|
26
|
+
function pathEq(a, b) {
|
|
27
|
+
if (process.platform === "win32" || process.platform === "darwin") {
|
|
28
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
return a === b;
|
|
31
|
+
}
|
|
32
|
+
function pathStartsWithDir(candidate, base) {
|
|
33
|
+
if (process.platform === "win32" || process.platform === "darwin") {
|
|
34
|
+
return candidate.toLowerCase().startsWith(base.toLowerCase() + sep.toLowerCase());
|
|
35
|
+
}
|
|
36
|
+
return candidate.startsWith(base + sep);
|
|
37
|
+
}
|
|
38
|
+
function isPathInside(target, root) {
|
|
39
|
+
const resolvedTarget = resolve(target);
|
|
40
|
+
const resolvedRoot = resolve(root);
|
|
41
|
+
if (pathEq(resolvedTarget, resolvedRoot)) return true;
|
|
42
|
+
return pathStartsWithDir(resolvedTarget, resolvedRoot);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/transformers-cache.ts
|
|
21
46
|
var log = createLogger("service");
|
|
22
47
|
var HF_CACHE_DIR_ENV = "GEZEL_HF_CACHE_DIR";
|
|
23
48
|
var TRANSFORMERS_MODULE = "@huggingface/transformers";
|
|
49
|
+
var DEFAULT_MODEL_LOCK_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
50
|
+
var DEFAULT_MODEL_LOCK_STALE_MS = 2 * 60 * 60 * 1e3;
|
|
51
|
+
var DEFAULT_MODEL_LOCK_POLL_MS = 100;
|
|
52
|
+
function modelLockDir(cacheDir, modelId) {
|
|
53
|
+
const key = createHash("sha256").update(modelId, "utf8").digest("hex").slice(0, 24);
|
|
54
|
+
return join2(cacheDir, ".gezel-locks", `${key}.lock`);
|
|
55
|
+
}
|
|
56
|
+
function cacheModelDir(cacheDir, modelId) {
|
|
57
|
+
const parts = modelId.split("/");
|
|
58
|
+
if (parts.length === 0 || parts.some((part) => !part || part === "." || part === ".." || part.includes("\\"))) {
|
|
59
|
+
throw new Error(`refusing unsafe transformers model id: ${modelId}`);
|
|
60
|
+
}
|
|
61
|
+
const root = resolve2(cacheDir);
|
|
62
|
+
const target = resolve2(root, ...parts);
|
|
63
|
+
if (target === root || !isPathInside(target, root)) {
|
|
64
|
+
throw new Error(`refusing transformers cache path outside ${root}: ${modelId}`);
|
|
65
|
+
}
|
|
66
|
+
return target;
|
|
67
|
+
}
|
|
68
|
+
function errorCode(err) {
|
|
69
|
+
return err?.code;
|
|
70
|
+
}
|
|
71
|
+
function delay(ms) {
|
|
72
|
+
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
|
|
73
|
+
}
|
|
74
|
+
async function staleOrMissing(lockDir, staleMs) {
|
|
75
|
+
try {
|
|
76
|
+
const info = await stat(lockDir);
|
|
77
|
+
return Date.now() - info.mtimeMs > staleMs;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
if (errorCode(err) === "ENOENT") return true;
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function withTransformersModelCacheLock(cacheDir, modelId, run, options = {}) {
|
|
84
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_MODEL_LOCK_TIMEOUT_MS;
|
|
85
|
+
const staleMs = options.staleMs ?? DEFAULT_MODEL_LOCK_STALE_MS;
|
|
86
|
+
const pollMs = options.pollMs ?? DEFAULT_MODEL_LOCK_POLL_MS;
|
|
87
|
+
const lockDir = modelLockDir(cacheDir, modelId);
|
|
88
|
+
await mkdir(dirname2(lockDir), { recursive: true });
|
|
89
|
+
const startedAt = Date.now();
|
|
90
|
+
while (true) {
|
|
91
|
+
try {
|
|
92
|
+
await mkdir(lockDir);
|
|
93
|
+
break;
|
|
94
|
+
} catch (err) {
|
|
95
|
+
if (errorCode(err) !== "EEXIST") throw err;
|
|
96
|
+
if (await staleOrMissing(lockDir, staleMs)) {
|
|
97
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
101
|
+
throw new Error(`timed out waiting for transformers cache lock for ${modelId}`);
|
|
102
|
+
}
|
|
103
|
+
await delay(pollMs);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
await writeFile(
|
|
108
|
+
join2(lockDir, "owner.json"),
|
|
109
|
+
`${JSON.stringify({ pid: process.pid, modelId, acquiredAt: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
110
|
+
`
|
|
111
|
+
);
|
|
112
|
+
return await run();
|
|
113
|
+
} finally {
|
|
114
|
+
await rm(lockDir, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async function removeTransformersModelCache(cacheDir, modelId) {
|
|
118
|
+
await rm(cacheModelDir(cacheDir, modelId), { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
function isCorruptTransformersCacheFailure(error) {
|
|
121
|
+
const messages = [];
|
|
122
|
+
let current = error;
|
|
123
|
+
for (let depth = 0; depth < 6 && current; depth++) {
|
|
124
|
+
messages.push(current instanceof Error ? current.message : String(current));
|
|
125
|
+
current = current instanceof Error ? current.cause : void 0;
|
|
126
|
+
}
|
|
127
|
+
return /(?:protobuf parsing failed|failed to parse protobuf|invalid protobuf|unexpected end of (?:file|data)|failed to load model|load model .* failed|invalid wire type)/i.test(
|
|
128
|
+
messages.join(" ")
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
async function loadTransformersModelWithCacheRecovery(cacheDir, modelId, load, onCorrupt) {
|
|
132
|
+
return withTransformersModelCacheLock(cacheDir, modelId, async () => {
|
|
133
|
+
try {
|
|
134
|
+
return await load();
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (!isCorruptTransformersCacheFailure(err)) throw err;
|
|
137
|
+
await onCorrupt?.();
|
|
138
|
+
await removeTransformersModelCache(cacheDir, modelId);
|
|
139
|
+
return await load();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
24
143
|
function isMissingModule(err, specifier) {
|
|
25
144
|
if (!(err instanceof Error)) return false;
|
|
26
145
|
const { code } = err;
|
|
@@ -124,7 +243,16 @@ async function loadPipeline() {
|
|
|
124
243
|
const { pipeline } = await import("@huggingface/transformers");
|
|
125
244
|
const modelId = embedModelId();
|
|
126
245
|
if (modelId !== DEFAULT_EMBED_MODEL) log2.info(`[embed] using model ${modelId}`);
|
|
127
|
-
|
|
246
|
+
const createPipeline = () => pipeline("feature-extraction", modelId, daemonLoadOptions());
|
|
247
|
+
if (!cacheDir) return await createPipeline();
|
|
248
|
+
return await loadTransformersModelWithCacheRecovery(
|
|
249
|
+
cacheDir,
|
|
250
|
+
modelId,
|
|
251
|
+
createPipeline,
|
|
252
|
+
() => {
|
|
253
|
+
log2.warn(`[embed] discarded corrupt cached model files for ${modelId}; retrying once`);
|
|
254
|
+
}
|
|
255
|
+
);
|
|
128
256
|
} catch (err) {
|
|
129
257
|
const missing = isMissingModule(err, TRANSFORMERS_MODULE);
|
|
130
258
|
const message = missing ? "Local memory embeddings are an optional npm feature. Install @huggingface/transformers@^3.8.1 alongside @bendyline/gezel-service (see the service README)." : err instanceof Error ? err.message : String(err);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { parentPort } from "worker_threads";
|
|
5
5
|
|
|
6
6
|
// src/memory/embed-core.ts
|
|
7
|
-
import { createHash } from "crypto";
|
|
7
|
+
import { createHash as createHash2 } from "crypto";
|
|
8
8
|
import { createLogger as createLogger2 } from "@bendyline/gezel";
|
|
9
9
|
import {
|
|
10
10
|
EmbedderUnavailableError,
|
|
@@ -15,12 +15,21 @@ import {
|
|
|
15
15
|
} from "@bendyline/gezel-knowledge";
|
|
16
16
|
|
|
17
17
|
// src/transformers-cache.ts
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
18
|
+
import { createHash } from "crypto";
|
|
19
|
+
import { mkdir, rm, stat, writeFile } from "fs/promises";
|
|
20
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
20
21
|
import { createLogger } from "@bendyline/gezel";
|
|
22
|
+
|
|
23
|
+
// src/fs/safe-paths.ts
|
|
24
|
+
import { realpath } from "fs/promises";
|
|
25
|
+
import { basename, dirname, isAbsolute, join, normalize, resolve, sep } from "path";
|
|
26
|
+
|
|
27
|
+
// src/transformers-cache.ts
|
|
21
28
|
var log = createLogger("service");
|
|
22
29
|
var HF_CACHE_DIR_ENV = "GEZEL_HF_CACHE_DIR";
|
|
23
30
|
var TRANSFORMERS_MODULE = "@huggingface/transformers";
|
|
31
|
+
var DEFAULT_MODEL_LOCK_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
32
|
+
var DEFAULT_MODEL_LOCK_STALE_MS = 2 * 60 * 60 * 1e3;
|
|
24
33
|
function isMissingModule(err, specifier) {
|
|
25
34
|
if (!(err instanceof Error)) return false;
|
|
26
35
|
const { code } = err;
|
|
@@ -268,7 +277,7 @@ import { open } from "fs/promises";
|
|
|
268
277
|
import { createRequire } from "module";
|
|
269
278
|
|
|
270
279
|
// src/index-store/image-meta.ts
|
|
271
|
-
import { createHash as
|
|
280
|
+
import { createHash as createHash3 } from "crypto";
|
|
272
281
|
import { inflateSync } from "zlib";
|
|
273
282
|
function readImageMeta(buf) {
|
|
274
283
|
if (buf.length < 24) return null;
|
|
@@ -329,7 +338,7 @@ function readImageStaticMeta(buf, opts) {
|
|
|
329
338
|
const meta = {
|
|
330
339
|
format: dims?.format ?? (looksLikeSvg(buf) ? "svg" : "unknown"),
|
|
331
340
|
byteLength: buf.length,
|
|
332
|
-
sha256:
|
|
341
|
+
sha256: createHash3("sha256").update(buf).digest("hex")
|
|
333
342
|
};
|
|
334
343
|
if (dims) {
|
|
335
344
|
meta.width = dims.width;
|
|
@@ -29,6 +29,7 @@ from __future__ import annotations
|
|
|
29
29
|
import json
|
|
30
30
|
import os
|
|
31
31
|
import time
|
|
32
|
+
from importlib import import_module
|
|
32
33
|
from pathlib import Path
|
|
33
34
|
from typing import Any, Optional
|
|
34
35
|
|
|
@@ -41,8 +42,16 @@ from mlx.utils import tree_flatten, tree_unflatten
|
|
|
41
42
|
# or some operator has both installed and a model uses one set vs the
|
|
42
43
|
# other. We resolve lazily on each load so an import failure here
|
|
43
44
|
# can't take down the server module at import time.
|
|
44
|
-
def _resolve_cache_class(name: str) -> Optional[type]:
|
|
45
|
+
def _resolve_cache_class(name: str, module_name: Optional[str] = None) -> Optional[type]:
|
|
45
46
|
candidates: list[Any] = []
|
|
47
|
+
# Custom caches such as Qwen4's QSAKVCache live in the model's language
|
|
48
|
+
# module, not either package's shared cache module. Persist the originating
|
|
49
|
+
# module and import it only inside the two trusted MLX package namespaces.
|
|
50
|
+
if module_name and module_name.startswith(("mlx_vlm.models.", "mlx_lm.models.")):
|
|
51
|
+
try:
|
|
52
|
+
candidates.append(import_module(module_name))
|
|
53
|
+
except Exception: # noqa: BLE001 — fall through to legacy lookup
|
|
54
|
+
pass
|
|
46
55
|
try:
|
|
47
56
|
from mlx_vlm.models import cache as vlm_cache # type: ignore[attr-defined]
|
|
48
57
|
|
|
@@ -64,8 +73,24 @@ def _resolve_cache_class(name: str) -> Optional[type]:
|
|
|
64
73
|
|
|
65
74
|
# Schema version baked into sidecar metadata. Bump when the on-disk
|
|
66
75
|
# layout changes incompatibly so older entries are rejected on load
|
|
67
|
-
# instead of silently corrupting reconstruction.
|
|
68
|
-
|
|
76
|
+
# instead of silently corrupting reconstruction. Version 2 stores scalar
|
|
77
|
+
# cache-state leaves in metadata: Qwen4's QSA cache carries an integer block
|
|
78
|
+
# compression ratio alongside its arrays, and passing that integer to
|
|
79
|
+
# ``mx.save_safetensors`` raises ``std::bad_cast``. Version 3 also records the
|
|
80
|
+
# defining module for model-specific caches such as QSAKVCache.
|
|
81
|
+
SCHEMA_VERSION = "3"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _split_state_leaves(layer_states: Any) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
85
|
+
"""Partition cache state into safetensor arrays and JSON scalar leaves."""
|
|
86
|
+
arrays: dict[str, Any] = {}
|
|
87
|
+
scalars: dict[str, Any] = {}
|
|
88
|
+
for path, value in tree_flatten(layer_states):
|
|
89
|
+
if value is None or isinstance(value, (bool, int, float, str)):
|
|
90
|
+
scalars[path] = value
|
|
91
|
+
else:
|
|
92
|
+
arrays[path] = value
|
|
93
|
+
return arrays, scalars
|
|
69
94
|
|
|
70
95
|
|
|
71
96
|
def cache_path(persist_dir: Path, model_fingerprint: str, cache_id: str) -> Path:
|
|
@@ -107,11 +132,12 @@ def save_cache(
|
|
|
107
132
|
# default to an empty dict so reconstruction can still proceed.
|
|
108
133
|
meta_state = [getattr(c, "meta_state", {}) for c in cache_layers]
|
|
109
134
|
class_names = [type(c).__name__ for c in cache_layers]
|
|
135
|
+
class_modules = [type(c).__module__ for c in cache_layers]
|
|
110
136
|
# tree_flatten turns a nested structure of arrays into a flat
|
|
111
137
|
# list of (path, array) tuples — exactly what save_safetensors
|
|
112
138
|
# wants. The path encoding is what tree_unflatten reads back to
|
|
113
139
|
# reconstruct the structure.
|
|
114
|
-
flat =
|
|
140
|
+
flat, state_scalars = _split_state_leaves(layer_states)
|
|
115
141
|
if not flat:
|
|
116
142
|
# Nothing array-shaped to save; reconstruction would always
|
|
117
143
|
# fail. Treat as a no-op rather than writing an empty file.
|
|
@@ -120,7 +146,9 @@ def save_cache(
|
|
|
120
146
|
metadata = {
|
|
121
147
|
"schema_version": SCHEMA_VERSION,
|
|
122
148
|
"classes": json.dumps(class_names),
|
|
149
|
+
"class_modules": json.dumps(class_modules),
|
|
123
150
|
"meta_state": json.dumps(meta_state, default=str),
|
|
151
|
+
"state_scalars": json.dumps(state_scalars),
|
|
124
152
|
"token_ids": json.dumps(token_ids),
|
|
125
153
|
"model_fingerprint": model_fingerprint,
|
|
126
154
|
"saved_at": str(time.time()),
|
|
@@ -193,17 +221,31 @@ def load_cache(
|
|
|
193
221
|
|
|
194
222
|
try:
|
|
195
223
|
class_names = json.loads(metadata.get("classes", "[]"))
|
|
224
|
+
class_modules = json.loads(metadata.get("class_modules", "[]"))
|
|
196
225
|
meta_state_list = json.loads(metadata.get("meta_state", "[]"))
|
|
197
226
|
token_ids = json.loads(metadata.get("token_ids", "[]"))
|
|
227
|
+
state_scalars = json.loads(metadata.get("state_scalars", "{}"))
|
|
198
228
|
except json.JSONDecodeError as exc:
|
|
199
229
|
print(f"[cache] metadata parse failed for cache_id={cache_id}: {exc}", flush=True)
|
|
200
230
|
return None
|
|
201
231
|
|
|
202
|
-
if
|
|
232
|
+
if (
|
|
233
|
+
not class_names
|
|
234
|
+
or len(class_modules) != len(class_names)
|
|
235
|
+
or not token_ids
|
|
236
|
+
or not isinstance(state_scalars, dict)
|
|
237
|
+
):
|
|
203
238
|
return None
|
|
204
239
|
|
|
205
240
|
try:
|
|
206
|
-
|
|
241
|
+
# Scalar leaves use the same tree paths as the mmap-backed arrays, so
|
|
242
|
+
# merging the two flat maps reconstructs the original cache state
|
|
243
|
+
# exactly. Reject an overlap rather than letting metadata replace a
|
|
244
|
+
# tensor silently.
|
|
245
|
+
if set(flat).intersection(state_scalars):
|
|
246
|
+
print(f"[cache] scalar/array path overlap for cache_id={cache_id}; ignoring", flush=True)
|
|
247
|
+
return None
|
|
248
|
+
layer_states = tree_unflatten([*flat.items(), *state_scalars.items()])
|
|
207
249
|
except Exception as exc: # noqa: BLE001
|
|
208
250
|
print(f"[cache] tree_unflatten failed for cache_id={cache_id}: {exc}", flush=True)
|
|
209
251
|
return None
|
|
@@ -217,8 +259,10 @@ def load_cache(
|
|
|
217
259
|
return None
|
|
218
260
|
|
|
219
261
|
cache_layers: list[Any] = []
|
|
220
|
-
for cls_name, meta, layer_state in zip(
|
|
221
|
-
|
|
262
|
+
for cls_name, cls_module, meta, layer_state in zip(
|
|
263
|
+
class_names, class_modules, meta_state_list, layer_states
|
|
264
|
+
):
|
|
265
|
+
cls = _resolve_cache_class(cls_name, cls_module)
|
|
222
266
|
if cls is None:
|
|
223
267
|
print(
|
|
224
268
|
f"[cache] unknown cache class {cls_name!r} for cache_id={cache_id}; ignoring",
|
|
@@ -80,6 +80,7 @@ import tool_call_stream # noqa: E402
|
|
|
80
80
|
import template_stability # noqa: E402
|
|
81
81
|
from lfm2_compat import ensure_lfm2_config_compat # noqa: E402
|
|
82
82
|
from qwen3_5_text_compat import is_text_only_qwen3_5_checkpoint # noqa: E402
|
|
83
|
+
from qwen4_ple import prepare_external_ple_view # noqa: E402
|
|
83
84
|
|
|
84
85
|
|
|
85
86
|
def log_contained_exception(tag: str) -> None:
|
|
@@ -266,6 +267,28 @@ def _tool_call_close_stop_ids(model_dir: str) -> "List[int]":
|
|
|
266
267
|
|
|
267
268
|
parser = argparse.ArgumentParser(description="gezel MLX wrapper server")
|
|
268
269
|
parser.add_argument("--model", required=True, help="Path to the MLX model directory")
|
|
270
|
+
parser.add_argument(
|
|
271
|
+
"--external-ple-dir",
|
|
272
|
+
type=str,
|
|
273
|
+
default=None,
|
|
274
|
+
help=(
|
|
275
|
+
"Derived model-view directory for Qwen4's disk-backed PLE table. "
|
|
276
|
+
"Applicable checkpoints are hard-linked into this view and the large "
|
|
277
|
+
"n-gram table is memory-mapped by mlx-vlm 0.7.1. Other model families "
|
|
278
|
+
"keep the source directory; an eligible Qwen4 checkpoint fails safely "
|
|
279
|
+
"when the configured runtime cannot prepare the view."
|
|
280
|
+
),
|
|
281
|
+
)
|
|
282
|
+
parser.add_argument(
|
|
283
|
+
"--allow-resident-ple-fallback",
|
|
284
|
+
action="store_true",
|
|
285
|
+
help=(
|
|
286
|
+
"Advanced/manual override: if a quantized Qwen4 checkpoint is eligible "
|
|
287
|
+
"for --external-ple-dir but the derived view cannot be prepared, load "
|
|
288
|
+
"the full PLE table resident. Gezel never passes this because its "
|
|
289
|
+
"catalog working-set reservation assumes external PLE is active."
|
|
290
|
+
),
|
|
291
|
+
)
|
|
269
292
|
parser.add_argument("--host", default="127.0.0.1")
|
|
270
293
|
parser.add_argument("--port", type=int, default=8080)
|
|
271
294
|
parser.add_argument(
|
|
@@ -508,6 +531,22 @@ _start_parent_death_watchdog()
|
|
|
508
531
|
|
|
509
532
|
print(f"Started server process [{os.getpid()}]", flush=True)
|
|
510
533
|
print("Waiting for application startup.", flush=True)
|
|
534
|
+
_SOURCE_MODEL_DIR = ARGS.model
|
|
535
|
+
ARGS.model, _ple_status, _ple_applicable = prepare_external_ple_view(
|
|
536
|
+
ARGS.model, ARGS.external_ple_dir
|
|
537
|
+
)
|
|
538
|
+
if ARGS.model != _SOURCE_MODEL_DIR:
|
|
539
|
+
print(f"[ple] external view {_ple_status}: {ARGS.model}", flush=True)
|
|
540
|
+
elif ARGS.external_ple_dir:
|
|
541
|
+
if not _ple_applicable:
|
|
542
|
+
print(f"[ple] not applicable: {_ple_status}", flush=True)
|
|
543
|
+
elif ARGS.allow_resident_ple_fallback:
|
|
544
|
+
print(f"[ple] resident fallback explicitly allowed: {_ple_status}", flush=True)
|
|
545
|
+
else:
|
|
546
|
+
raise RuntimeError(
|
|
547
|
+
"external Qwen4 PLE view is required by the planned memory footprint "
|
|
548
|
+
f"but could not be prepared: {_ple_status}"
|
|
549
|
+
)
|
|
511
550
|
print(f"Loading model from: {ARGS.model}", flush=True)
|
|
512
551
|
_LFM2_FF_DIM = ensure_lfm2_config_compat(ARGS.model)
|
|
513
552
|
if _LFM2_FF_DIM is not None:
|
|
@@ -729,7 +768,11 @@ def _metal_recommended_working_set() -> int:
|
|
|
729
768
|
"""Metal's `max_recommended_working_set_size` (bytes), or 0 if unavailable.
|
|
730
769
|
This is the OS's own soft ceiling for GPU-resident memory."""
|
|
731
770
|
try:
|
|
732
|
-
|
|
771
|
+
# mlx 0.32.2 (resolved by mlx-vlm 0.7.1) deprecates the old
|
|
772
|
+
# mx.metal.device_info() namespace. Keep the fallback so an operator's
|
|
773
|
+
# older, explicitly configured mlx package still gets a useful cap.
|
|
774
|
+
device_info = getattr(mx, "device_info", None)
|
|
775
|
+
info = device_info() if callable(device_info) else mx.metal.device_info()
|
|
733
776
|
return int(info.get("max_recommended_working_set_size", 0) or 0)
|
|
734
777
|
except Exception: # noqa: BLE001 — API shape varies across mlx versions
|
|
735
778
|
return 0
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Prepare MLX-VLM 0.7.1's disk-backed Qwen4 PLE model view.
|
|
2
|
+
|
|
3
|
+
Qwen3.8 Flash Next's hashed n-gram table is tens of GiB. Loading every row
|
|
4
|
+
into Metal leaves a 128 GiB Mac with very little room for KV and activations,
|
|
5
|
+
even though a turn touches only a tiny fraction of those rows. mlx-vlm 0.7.1
|
|
6
|
+
ships ``prepare_external_ple_model`` and ``QuantizedMMapNGramEmbedding`` for
|
|
7
|
+
this exact checkpoint shape.
|
|
8
|
+
|
|
9
|
+
The view contains hard links to the ordinary model shards plus a small index
|
|
10
|
+
that excludes resident PLE tensors. The original install stays immutable and
|
|
11
|
+
is still the source of mmap reads. Publication is lock-protected and atomic,
|
|
12
|
+
so concurrent replicas cannot observe a half-built view.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import fcntl
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import shutil
|
|
22
|
+
import uuid
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Callable, Optional, Tuple
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
PLE_MARKER = ".ple.ple_embedding.ngram_embedding.shards."
|
|
28
|
+
VIEW_MARKER = "GEZEL_PLE_VIEW.json"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _read_json(path: Path):
|
|
32
|
+
try:
|
|
33
|
+
return json.loads(path.read_text())
|
|
34
|
+
except (OSError, ValueError, TypeError):
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _file_sha256(path: Path) -> str:
|
|
39
|
+
digest = hashlib.sha256()
|
|
40
|
+
with path.open("rb") as stream:
|
|
41
|
+
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
|
42
|
+
digest.update(chunk)
|
|
43
|
+
return digest.hexdigest()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def checkpoint_signature(source: Path) -> Optional[dict]:
|
|
47
|
+
"""Return the cheap immutable identity used to validate a derived view."""
|
|
48
|
+
config_path = source / "config.json"
|
|
49
|
+
index_path = source / "model.safetensors.index.json"
|
|
50
|
+
config = _read_json(config_path)
|
|
51
|
+
index = _read_json(index_path)
|
|
52
|
+
if not isinstance(config, dict) or not isinstance(index, dict):
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
text_config = config.get("text_config")
|
|
56
|
+
if not isinstance(text_config, dict):
|
|
57
|
+
text_config = {}
|
|
58
|
+
model_types = {str(config.get("model_type", "")), str(text_config.get("model_type", ""))}
|
|
59
|
+
if not ({"qwen4_exp", "qwen4_exp_text"} & model_types):
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
weight_map = index.get("weight_map")
|
|
63
|
+
if not isinstance(weight_map, dict) or not any(PLE_MARKER in str(key) for key in weight_map):
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
"source": str(source.resolve()),
|
|
68
|
+
"config_sha256": _file_sha256(config_path),
|
|
69
|
+
"index_sha256": _file_sha256(index_path),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _valid_view(target: Path, signature: dict) -> bool:
|
|
74
|
+
marker = _read_json(target / VIEW_MARKER)
|
|
75
|
+
config = _read_json(target / "config.json")
|
|
76
|
+
ple_manifest = target / "ple-store.json"
|
|
77
|
+
if marker != signature or not isinstance(config, dict) or not ple_manifest.is_file():
|
|
78
|
+
return False
|
|
79
|
+
text_config = config.get("text_config")
|
|
80
|
+
ple_storage = text_config.get("ple_storage") if isinstance(text_config, dict) else None
|
|
81
|
+
return isinstance(ple_storage, dict) and ple_storage.get("manifest") == ple_manifest.name
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _same_device_target(source: Path, requested: Path) -> Path:
|
|
85
|
+
"""Prefer the managed view root, then a same-volume sibling if needed."""
|
|
86
|
+
requested.parent.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
if source.stat().st_dev == requested.parent.stat().st_dev:
|
|
88
|
+
return requested
|
|
89
|
+
sibling = source.parent / ".gezel-views" / requested.name
|
|
90
|
+
sibling.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
return sibling
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def prepare_external_ple_view(
|
|
95
|
+
source_dir: str,
|
|
96
|
+
requested_view_dir: Optional[str],
|
|
97
|
+
*,
|
|
98
|
+
prepare_fn: Optional[Callable[..., object]] = None,
|
|
99
|
+
) -> Tuple[str, str, bool]:
|
|
100
|
+
"""Return ``(effective_model_dir, status, applicable)``.
|
|
101
|
+
|
|
102
|
+
Any unsupported shape or contained preparation failure returns the source
|
|
103
|
+
directory plus a diagnostic status. The caller decides whether a resident
|
|
104
|
+
fallback is safe for its memory reservation; Gezel's managed launcher
|
|
105
|
+
requires the view for applicable checkpoints.
|
|
106
|
+
"""
|
|
107
|
+
source = Path(source_dir).expanduser().resolve()
|
|
108
|
+
if not requested_view_dir:
|
|
109
|
+
return str(source), "disabled", False
|
|
110
|
+
try:
|
|
111
|
+
signature = checkpoint_signature(source)
|
|
112
|
+
except OSError as exc:
|
|
113
|
+
return str(source), f"signature unavailable: {exc}", False
|
|
114
|
+
if signature is None:
|
|
115
|
+
return str(source), "not a quantized Qwen4 PLE checkpoint", False
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
target = _same_device_target(source, Path(requested_view_dir).expanduser().resolve())
|
|
119
|
+
except OSError as exc:
|
|
120
|
+
return str(source), f"view root unavailable: {exc}", True
|
|
121
|
+
|
|
122
|
+
lock_path = target.parent / f".{target.name}.lock"
|
|
123
|
+
try:
|
|
124
|
+
with lock_path.open("a+b") as lock:
|
|
125
|
+
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
|
|
126
|
+
if _valid_view(target, signature):
|
|
127
|
+
return str(target), "reused", True
|
|
128
|
+
|
|
129
|
+
# A SIGKILL during an earlier publication can leave a sibling
|
|
130
|
+
# staging directory whose hard links would otherwise retain the
|
|
131
|
+
# checkpoint after a later model deletion. The per-target lock
|
|
132
|
+
# makes these unambiguously stale here.
|
|
133
|
+
for stale in target.parent.glob(f".{target.name}.*.partial"):
|
|
134
|
+
if stale.is_dir():
|
|
135
|
+
shutil.rmtree(stale, ignore_errors=True)
|
|
136
|
+
|
|
137
|
+
if prepare_fn is None:
|
|
138
|
+
try:
|
|
139
|
+
from mlx_vlm.models.qwen4_exp.ple_storage import (
|
|
140
|
+
prepare_external_ple_model,
|
|
141
|
+
)
|
|
142
|
+
except Exception as exc: # noqa: BLE001 - optional across configured runtimes
|
|
143
|
+
return str(source), f"mlx-vlm external PLE unavailable: {exc}", True
|
|
144
|
+
prepare_fn = prepare_external_ple_model
|
|
145
|
+
|
|
146
|
+
if target.exists():
|
|
147
|
+
shutil.rmtree(target)
|
|
148
|
+
staging = target.parent / f".{target.name}.{os.getpid()}.{uuid.uuid4().hex}.partial"
|
|
149
|
+
try:
|
|
150
|
+
prepare_fn(source, staging, cache_rows=0)
|
|
151
|
+
(staging / VIEW_MARKER).write_text(json.dumps(signature, indent=2) + "\n")
|
|
152
|
+
os.rename(staging, target)
|
|
153
|
+
finally:
|
|
154
|
+
shutil.rmtree(staging, ignore_errors=True)
|
|
155
|
+
if not _valid_view(target, signature):
|
|
156
|
+
return str(source), "prepared view failed validation", True
|
|
157
|
+
return str(target), "prepared", True
|
|
158
|
+
except Exception as exc: # noqa: BLE001 - report a contained preparation failure
|
|
159
|
+
return str(source), f"preparation failed: {exc}", True
|