@openspecui/server 3.11.0 → 3.11.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/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { BatchTranslateInputSchema, CliExecutor, CodeEditorThemeSchema, ConfigManager, CustomSoundHashSchema, DASHBOARD_METRIC_KEYS, DashboardConfigSchema, DocumentTranslationConfigSchema, GitConfigSchema, GlobalSettingsManager, LocalModelAssetStateSchema, MarkdownParser, NotificationPublishInputSchema, NotificationSettingsSchema, OPENSPECUI_HOOKS_VERSION, OpenSpecAdapter, OpenSpecUIGlobalSettingsSchema, OpenSpecWatcher, OpsxConfigSchema, OpsxKernel, PtyClientMessageSchema, ReactiveContext, ServiceTranslationEngineIdSchema, TRANSLATION_ENGINE_MANIFESTS, TerminalConfigSchema, TerminalControlParser, TerminalRendererEngineSchema, TranslationCacheReadInputSchema, TranslationCacheSettingsSchema, TranslationCacheWriteInputSchema, TranslationEngineIdSchema, TranslationLocalSettingsSchema, TranslationOpenAISettingsSchema, buildBackendHealthPayload, buildLocalDownloadPlanFromRepositoryFiles, clearCache, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, getDetectedProjectTools, getOpsxEntityRootRelativePath, getToolInitStates, getWatcherRuntimeStatus, inferFileMime, inferFilePreviewKind, initWatcherPool, isWatcherPoolInitialized, normalizeOpsxEntityPath, parseOpsxEntityMetadata, parseOpsxSchemaDetail, resolveTerminalShellDefaults, selectLocalDownloadGroup, sniffGlobalCli, subscribeWatcherRuntimeStatus, terminalNotificationEventToPublishInput } from "@openspecui/core";
1
+ import { BatchTranslateInputSchema, CliExecutor, CodeEditorThemeSchema, ConfigManager, CustomSoundHashSchema, DASHBOARD_METRIC_KEYS, DashboardConfigSchema, DocumentTranslationConfigSchema, GitConfigSchema, GlobalSettingsManager, LocalModelAssetStateSchema, LocalModelLifecycleFileStateSchema, LocalModelLifecycleGroupStateSchema, LocalModelProfileManifestSchema, MarkdownParser, NotificationPublishInputSchema, NotificationSettingsSchema, OPENSPECUI_HOOKS_VERSION, OpenSpecAdapter, OpenSpecUIGlobalSettingsSchema, OpenSpecWatcher, OpsxConfigSchema, OpsxKernel, PtyClientMessageSchema, ReactiveContext, ServiceTranslationEngineIdSchema, TRANSLATION_ENGINE_MANIFESTS, TerminalConfigSchema, TerminalControlParser, TerminalRendererEngineSchema, TranslationCacheReadInputSchema, TranslationCacheSettingsSchema, TranslationCacheWriteInputSchema, TranslationEngineIdSchema, TranslationLocalSettingsSchema, TranslationOpenAISettingsSchema, buildBackendHealthPayload, buildLocalDownloadPlanFromRepositoryFiles, checkLocalDirectionalModelLanguagePair, clearCache, getAllTools, getAvailableTools, getConfiguredTools, getDefaultCliCommandString, getDetectedProjectTools, getOpsxEntityRootRelativePath, getToolInitStates, getWatcherRuntimeStatus, inferFileMime, inferFilePreviewKind, initWatcherPool, isWatcherPoolInitialized, normalizeOpsxEntityPath, parseOpsxEntityMetadata, parseOpsxSchemaDetail, resolveTerminalShellDefaults, selectLocalDownloadGroup, sniffGlobalCli, subscribeWatcherRuntimeStatus, terminalNotificationEventToPublishInput } from "@openspecui/core";
2
2
  import { basename, dirname, extname, join, matchesGlob, relative, resolve, sep } from "node:path";
3
3
  import { access, copyFile, lstat, mkdir, open, readFile, readlink, realpath, rename, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -16,7 +16,7 @@ import { homedir, platform } from "node:os";
16
16
  import { EventEmitter } from "node:events";
17
17
  import { execFile } from "node:child_process";
18
18
  import { promisify } from "node:util";
19
- import { downloadFile, fileDownloadInfo, listFiles } from "@huggingface/hub";
19
+ import { downloadFile, fileDownloadInfo, listFiles, modelInfo } from "@huggingface/hub";
20
20
  import { observable } from "@trpc/server/observable";
21
21
  import { z } from "zod";
22
22
  import { Agent, EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
@@ -357,7 +357,7 @@ function normalizeHooksModule(moduleValue) {
357
357
  onRunWorkflow: isOnRunWorkflowHook(record.onRunWorkflow) ? record.onRunWorkflow : isOnRunWorkflowHook(defaultRecord.onRunWorkflow) ? defaultRecord.onRunWorkflow : isOnRunWorkflowHook(moduleExportsRecord.onRunWorkflow) ? moduleExportsRecord.onRunWorkflow : void 0
358
358
  };
359
359
  }
360
- async function pathExists$2(path) {
360
+ async function pathExists$1(path) {
361
361
  try {
362
362
  await access(path);
363
363
  return true;
@@ -386,8 +386,8 @@ var ProjectHookRuntime = class {
386
386
  await Promise.allSettled(callbacks.map((cleanup) => cleanup()));
387
387
  }
388
388
  async loadFresh() {
389
- if (!await pathExists$2(this.hooksPath)) return {};
390
- const { tsImport } = await import("tsx/esm/api");
389
+ if (!await pathExists$1(this.hooksPath)) return {};
390
+ const { tsImport } = await import("./api-C9I67iTa.mjs");
391
391
  return normalizeHooksModule(await tsImport(`${pathToFileURL(this.hooksPath).href}?t=${Date.now()}`, { parentURL: pathToFileURL(this.hooksPath).href }));
392
392
  }
393
393
  };
@@ -799,7 +799,7 @@ function parseRelatedChanges(paths) {
799
799
  continue;
800
800
  }
801
801
  }
802
- return [...related].sort((a, b) => a.localeCompare(b));
802
+ return [...related].sort((a$1, b) => a$1.localeCompare(b));
803
803
  }
804
804
  async function resolveDefaultBranch(projectDir, runGit) {
805
805
  const remoteHead = await runGit(projectDir, [
@@ -1067,9 +1067,9 @@ async function buildDashboardGitSnapshot(options) {
1067
1067
  maxCommitEntries,
1068
1068
  readPathTimestampMs
1069
1069
  })));
1070
- worktrees.sort((a, b) => {
1071
- if (a.isCurrent !== b.isCurrent) return a.isCurrent ? -1 : 1;
1072
- return a.branchName.localeCompare(b.branchName);
1070
+ worktrees.sort((a$1, b) => {
1071
+ if (a$1.isCurrent !== b.isCurrent) return a$1.isCurrent ? -1 : 1;
1072
+ return a$1.branchName.localeCompare(b.branchName);
1073
1073
  });
1074
1074
  return {
1075
1075
  defaultBranch,
@@ -1092,7 +1092,7 @@ function createEmptyTrendSeries() {
1092
1092
  return Object.fromEntries(DASHBOARD_METRIC_KEYS.map((metric) => [metric, []]));
1093
1093
  }
1094
1094
  function normalizeEvents(events, pointLimit) {
1095
- return events.filter((event) => Number.isFinite(event.ts) && event.ts > 0 && Number.isFinite(event.value)).sort((a, b) => a.ts - b.ts).slice(-pointLimit);
1095
+ return events.filter((event) => Number.isFinite(event.ts) && event.ts > 0 && Number.isFinite(event.value)).sort((a$1, b) => a$1.ts - b.ts).slice(-pointLimit);
1096
1096
  }
1097
1097
  function buildTimeWindow(options) {
1098
1098
  const { probeEvents, targetBars, rightEdgeTs } = options;
@@ -1615,9 +1615,6 @@ function normalizeHuggingFaceEndpoint(endpoint) {
1615
1615
  function buildHuggingFaceApiBaseUrl(endpoint) {
1616
1616
  return `${normalizeHuggingFaceEndpoint(endpoint)}/api`;
1617
1617
  }
1618
- function buildTransformersRemoteHost(endpoint) {
1619
- return `${normalizeHuggingFaceEndpoint(endpoint)}/`;
1620
- }
1621
1618
 
1622
1619
  //#endregion
1623
1620
  //#region src/local-model-asset-store.ts
@@ -1686,9 +1683,21 @@ function getDefaultLocalModelCacheDir() {
1686
1683
  function getDefaultLocalModelIndexPath() {
1687
1684
  return join(getDefaultLocalModelCacheRoot(), "models.json");
1688
1685
  }
1686
+ function getDefaultLocalModelProfileManifestPath() {
1687
+ return join(getDefaultLocalModelCacheRoot(), "profile-manifests.json");
1688
+ }
1689
1689
  function getDefaultLocalModelFetchCachePath() {
1690
1690
  return join(getDefaultLocalModelCacheRoot(), "fetch-cache.json");
1691
1691
  }
1692
+ function getLocalModelProfileRoot(cacheDir, modelId) {
1693
+ return join(cacheDir, "profiles", sanitizeLocalModelPathSegment(modelId));
1694
+ }
1695
+ function getLocalModelProfileGroupRoot(cacheDir, modelId, groupId) {
1696
+ return join(getLocalModelProfileRoot(cacheDir, modelId), sanitizeLocalModelPathSegment(groupId));
1697
+ }
1698
+ function sanitizeLocalModelPathSegment(value) {
1699
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "-");
1700
+ }
1692
1701
 
1693
1702
  //#endregion
1694
1703
  //#region src/local-model-fetch-cache-store.ts
@@ -1841,60 +1850,80 @@ function getTransformersLocalModelPath(cacheDir, modelId) {
1841
1850
  function getTransformersFileCacheModelPath(cacheDir, modelId) {
1842
1851
  return join(cacheDir, modelId);
1843
1852
  }
1844
- async function readLocalModelFileStatus(input) {
1845
- const files = await Promise.all(input.files.map(async (file) => ({
1846
- file,
1847
- cached: await pathExists$1(join(getTransformersLocalModelPath(input.cacheDir, input.modelId), file)) || await pathExists$1(join(getTransformersFileCacheModelPath(input.cacheDir, input.modelId), file))
1848
- })));
1849
- return {
1850
- allCached: files.length > 0 && files.every((file) => file.cached),
1851
- files
1852
- };
1853
- }
1854
- async function pathExists$1(path) {
1855
- return stat(path).then(() => true, () => false);
1856
- }
1853
+
1854
+ //#endregion
1855
+ //#region src/local-model-profile-manifest-store.ts
1856
+ const LocalModelProfileManifestIndexSchema = z.object({
1857
+ version: z.literal(1).default(1),
1858
+ manifests: z.array(LocalModelProfileManifestSchema).default([])
1859
+ });
1860
+ var LocalModelProfileManifestStore = class {
1861
+ constructor(options) {
1862
+ this.options = options;
1863
+ }
1864
+ getManifestPath() {
1865
+ return this.options.manifestPath;
1866
+ }
1867
+ async readAll() {
1868
+ return (await this.readFile()).manifests;
1869
+ }
1870
+ async readMap() {
1871
+ return new Map((await this.readAll()).map((manifest) => [manifest.modelId, manifest]));
1872
+ }
1873
+ async read(modelId) {
1874
+ return (await this.readMap()).get(modelId) ?? null;
1875
+ }
1876
+ async writeAll(manifests) {
1877
+ const normalized = LocalModelProfileManifestIndexSchema.parse({
1878
+ version: 1,
1879
+ manifests: [...manifests].sort((left, right) => left.modelId.localeCompare(right.modelId))
1880
+ });
1881
+ const serialized = JSON.stringify(normalized, null, 2);
1882
+ await mkdir(dirname(this.options.manifestPath), { recursive: true });
1883
+ const tempPath = `${this.options.manifestPath}.${process.pid}.${Date.now()}.tmp`;
1884
+ await writeFile(tempPath, `${serialized}\n`, "utf8");
1885
+ await rename(tempPath, this.options.manifestPath);
1886
+ clearCache();
1887
+ }
1888
+ async upsert(manifest) {
1889
+ const manifests = await this.readMap();
1890
+ manifests.set(manifest.modelId, LocalModelProfileManifestSchema.parse(manifest));
1891
+ await this.writeAll([...manifests.values()]);
1892
+ }
1893
+ async remove(modelId) {
1894
+ const manifests = await this.readMap();
1895
+ if (!manifests.delete(modelId)) return;
1896
+ await this.writeAll([...manifests.values()]);
1897
+ }
1898
+ async readFile() {
1899
+ try {
1900
+ const content = await readFile(this.options.manifestPath, "utf8");
1901
+ const parsed = JSON.parse(content);
1902
+ const result = LocalModelProfileManifestIndexSchema.safeParse(parsed);
1903
+ return result.success ? result.data : LocalModelProfileManifestIndexSchema.parse({});
1904
+ } catch {
1905
+ return LocalModelProfileManifestIndexSchema.parse({});
1906
+ }
1907
+ }
1908
+ };
1857
1909
 
1858
1910
  //#endregion
1859
1911
  //#region src/local-model-runtime.ts
1860
- async function configureTransformersRuntime(transformers, cacheDir) {
1861
- await mkdir(cacheDir, { recursive: true });
1862
- transformers.env.cacheDir = cacheDir;
1863
- transformers.env.allowLocalModels = false;
1864
- transformers.env.localModelPath = join(cacheDir, "models");
1865
- }
1866
- async function resolveLocalModelRuntimePlan(input) {
1867
- await configureTransformersRuntime(input.transformers, input.cacheDir);
1868
- const hubUrl = normalizeHuggingFaceEndpoint(input.hfEndpoint);
1869
- const repositoryFiles = await readHuggingFaceRepositoryFiles({
1870
- selectedGroupId: input.selectedGroupId,
1912
+ async function readLocalModelRepositorySnapshot(input) {
1913
+ return readHuggingFaceRepositorySnapshot({
1871
1914
  modelId: input.modelId,
1872
- hubUrl,
1873
- fetchCacheStore: input.fetchCacheStore
1874
- });
1875
- return buildLocalDownloadPlanFromRepositoryFiles({
1876
- modelId: input.modelId,
1877
- selectedGroupId: input.selectedGroupId,
1878
- files: repositoryFiles
1915
+ hubUrl: normalizeHuggingFaceEndpoint(input.hfEndpoint),
1916
+ fetchCacheStore: input.fetchCacheStore,
1917
+ revision: input.revision
1879
1918
  });
1880
1919
  }
1881
- async function resolveLocalModelRuntimePlanFromProject(input) {
1882
- const transformers = await (input.loadTransformersModule ?? loadLocalTransformersModule)(input.projectDir, input.globalSettingsManager);
1883
- const settings = await input.globalSettingsManager.readSettings();
1884
- return resolveLocalModelRuntimePlan({
1885
- modelId: input.modelId,
1886
- transformers,
1887
- cacheDir: input.cacheDir ?? getDefaultLocalModelCacheDir(),
1888
- selectedGroupId: input.selectedGroupId,
1889
- hfEndpoint: settings.translationEngines.local?.hfEndpoint,
1890
- fetchCacheStore: input.fetchCacheStore
1891
- });
1892
- }
1893
- async function readHuggingFaceRepositoryFiles(input) {
1920
+ async function readHuggingFaceRepositorySnapshot(input) {
1921
+ const detail = await readHuggingFaceModelSnapshotInfo(input).catch(() => null);
1894
1922
  let lastError;
1895
1923
  for (let attempt = 0; attempt < 3; attempt += 1) {
1896
1924
  try {
1897
1925
  const files = [];
1926
+ let commitHash = detail?.commitHash;
1898
1927
  for await (const entry of listFiles({
1899
1928
  repo: {
1900
1929
  type: "model",
@@ -1902,16 +1931,30 @@ async function readHuggingFaceRepositoryFiles(input) {
1902
1931
  },
1903
1932
  recursive: true,
1904
1933
  expand: true,
1934
+ revision: input.revision,
1905
1935
  hubUrl: input.hubUrl,
1906
1936
  fetch: input.fetchCacheStore ? createProviderFetchCache(input.fetchCacheStore) : void 0
1907
1937
  })) {
1908
1938
  if (entry.type !== "file") continue;
1939
+ commitHash ??= entry.lastCommit?.id;
1940
+ const etag = entry.lfs?.oid ?? entry.xetHash ?? entry.oid;
1909
1941
  files.push({
1910
1942
  path: entry.path,
1911
- sizeBytes: entry.lfs?.size ?? entry.size
1943
+ sizeBytes: entry.lfs?.size ?? entry.size,
1944
+ etag,
1945
+ revision: entry.lastCommit?.id,
1946
+ sourceUrl: `${input.hubUrl}/${input.modelId}/resolve/${entry.lastCommit?.id ?? input.revision ?? "main"}/${entry.path}`,
1947
+ raw: entry
1912
1948
  });
1913
1949
  }
1914
- if (files.length > 0) return files;
1950
+ if (files.length > 0 && commitHash) return {
1951
+ modelId: input.modelId,
1952
+ revision: input.revision ?? "main",
1953
+ commitHash,
1954
+ shortCommitHash: commitHash.slice(0, 6),
1955
+ files,
1956
+ raw: detail?.raw
1957
+ };
1915
1958
  lastError = /* @__PURE__ */ new Error(`No repository files were returned for ${input.modelId}.`);
1916
1959
  } catch (error) {
1917
1960
  lastError = error;
@@ -1919,17 +1962,40 @@ async function readHuggingFaceRepositoryFiles(input) {
1919
1962
  if (attempt < 2) await delay$2(300 * (attempt + 1));
1920
1963
  }
1921
1964
  const cachedFiles = await readCachedHuggingFaceRepositoryFiles(input);
1922
- if (cachedFiles.length > 0) return cachedFiles;
1965
+ if (cachedFiles.files.length > 0 && cachedFiles.commitHash) return {
1966
+ modelId: input.modelId,
1967
+ revision: input.revision ?? "main",
1968
+ commitHash: cachedFiles.commitHash,
1969
+ shortCommitHash: cachedFiles.commitHash.slice(0, 6),
1970
+ files: cachedFiles.files,
1971
+ raw: cachedFiles.raw
1972
+ };
1923
1973
  throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error(`Unable to read repository files for ${input.modelId}.`);
1924
1974
  }
1975
+ async function readHuggingFaceModelSnapshotInfo(input) {
1976
+ const detail = await modelInfo({
1977
+ name: input.modelId,
1978
+ hubUrl: input.hubUrl,
1979
+ revision: input.revision,
1980
+ additionalFields: ["sha"]
1981
+ });
1982
+ const raw = detail;
1983
+ const commitHash = typeof detail.sha === "string" ? detail.sha : void 0;
1984
+ if (!commitHash) throw new Error(`Unable to resolve a commit hash for ${input.modelId}.`);
1985
+ return {
1986
+ commitHash,
1987
+ raw
1988
+ };
1989
+ }
1925
1990
  async function readCachedHuggingFaceRepositoryFiles(input) {
1926
- if (!input.fetchCacheStore) return [];
1991
+ if (!input.fetchCacheStore) return { files: [] };
1927
1992
  const record = await input.fetchCacheStore.read(input.modelId);
1928
- if (!record?.detailRaw) return [];
1993
+ if (!record?.detailRaw) return { files: [] };
1929
1994
  return extractRepositoryFilesFromCachedDetail(record.detailRaw);
1930
1995
  }
1931
1996
  function extractRepositoryFilesFromCachedDetail(raw) {
1932
1997
  const siblings = Array.isArray(raw.siblings) ? raw.siblings : [];
1998
+ const commitHash = typeof raw.sha === "string" ? raw.sha : void 0;
1933
1999
  const files = [];
1934
2000
  for (const sibling of siblings) {
1935
2001
  if (!sibling || typeof sibling !== "object") continue;
@@ -1938,10 +2004,16 @@ function extractRepositoryFilesFromCachedDetail(raw) {
1938
2004
  if (!path) continue;
1939
2005
  files.push({
1940
2006
  path,
1941
- sizeBytes: typeof record.size === "number" && Number.isFinite(record.size) ? record.size : void 0
2007
+ sizeBytes: typeof record.size === "number" && Number.isFinite(record.size) ? record.size : void 0,
2008
+ revision: commitHash,
2009
+ raw: record
1942
2010
  });
1943
2011
  }
1944
- return files;
2012
+ return {
2013
+ files,
2014
+ commitHash,
2015
+ raw
2016
+ };
1945
2017
  }
1946
2018
  function createProviderFetchCache(fetchCacheStore) {
1947
2019
  return async (input, init) => {
@@ -1969,9 +2041,6 @@ function headersToRecord$1(headers) {
1969
2041
  function delay$2(ms) {
1970
2042
  return new Promise((resolve$1) => setTimeout(resolve$1, ms));
1971
2043
  }
1972
- async function loadLocalTransformersModule(_projectDir, _globalSettingsManager) {
1973
- return await import("@huggingface/transformers");
1974
- }
1975
2044
 
1976
2045
  //#endregion
1977
2046
  //#region src/network-dispatcher.ts
@@ -2421,6 +2490,7 @@ const DEFAULT_NETWORK_RETRY_DELAY_MAX_MS = 5e3;
2421
2490
  var LocalModelAssetService = class {
2422
2491
  now;
2423
2492
  store;
2493
+ profileManifestStore;
2424
2494
  cacheDir;
2425
2495
  fetchCacheStore;
2426
2496
  networkRetryPolicy;
@@ -2440,6 +2510,7 @@ var LocalModelAssetService = class {
2440
2510
  maxDelayMs: options.networkRetryPolicy?.maxDelayMs ?? DEFAULT_NETWORK_RETRY_DELAY_MAX_MS
2441
2511
  };
2442
2512
  this.store = new LocalModelAssetStore({ indexPath: options.indexPath ?? getDefaultLocalModelIndexPath() });
2513
+ this.profileManifestStore = new LocalModelProfileManifestStore({ manifestPath: options.profileManifestPath ?? getDefaultLocalModelProfileManifestPath() });
2443
2514
  this.fetchCacheStore = new LocalModelFetchCacheStore({
2444
2515
  cachePath: options.fetchCachePath ?? getDefaultLocalModelFetchCachePath(),
2445
2516
  now: this.now
@@ -2541,106 +2612,129 @@ var LocalModelAssetService = class {
2541
2612
  async readSelectedModelState(modelId, selectedGroupId) {
2542
2613
  const state = (await this.store.readMap()).get(modelId);
2543
2614
  if (state) return this.refreshCachedState(state, selectedGroupId);
2544
- const session = this.sessions.get(modelId);
2545
- if (session) {
2546
- const selected = modelId === await this.readSelectedModel();
2547
- const plan = await this.readPlanForState(modelId, selectedGroupId ?? session.selectedGroupId);
2548
- const selectedGroup = selectLocalDownloadGroup(plan, selectedGroupId ?? session.selectedGroupId);
2549
- const files = (selectedGroup?.files ?? plan?.files ?? []).map((file) => ({
2550
- path: file.path,
2551
- sizeBytes: file.sizeBytes,
2552
- downloadedBytes: 0
2553
- }));
2554
- return LocalModelAssetStateSchema.parse({
2555
- modelId,
2556
- plan: plan ?? void 0,
2557
- status: "downloading",
2558
- selected,
2559
- resumable: true,
2560
- totalBytes: selectedGroup?.estimatedTotalBytes ?? plan?.estimatedTotalBytes,
2561
- progress: 0,
2562
- files,
2563
- updatedAt: this.now()
2564
- });
2565
- }
2566
- return LocalModelAssetStateSchema.parse({
2615
+ const selected = modelId === await this.readSelectedModel();
2616
+ const baseState = LocalModelAssetStateSchema.parse({
2567
2617
  modelId,
2568
2618
  status: "not-downloaded",
2569
- selected: modelId === await this.readSelectedModel(),
2619
+ selected,
2620
+ selectedGroupId,
2570
2621
  updatedAt: this.now()
2571
2622
  });
2623
+ return this.refreshCachedState(baseState, selectedGroupId);
2572
2624
  }
2573
- async startDownload(modelId, selectedGroupId) {
2574
- return this.runDownload(modelId, "downloading", "Downloading local model", selectedGroupId);
2625
+ async startDownload(modelId, groupId) {
2626
+ return this.runDownload(modelId, "downloading", "Downloading local model", groupId);
2575
2627
  }
2576
- async resumeDownload(modelId, selectedGroupId) {
2577
- return this.runDownload(modelId, "downloading", "Resuming local model download", selectedGroupId);
2628
+ async resumeDownload(modelId, groupId) {
2629
+ return this.runDownload(modelId, "downloading", "Resuming local model download", groupId);
2578
2630
  }
2579
- async pauseDownload(modelId) {
2580
- const session = this.sessions.get(modelId);
2631
+ async pauseDownload(modelId, groupId) {
2632
+ const requestedGroupId = groupId ?? await this.readSelectedGroupId();
2633
+ if (!requestedGroupId) return { success: true };
2634
+ const current = await this.readSelectedModelState(modelId, requestedGroupId);
2635
+ const effectiveGroupId = current.plan?.selectedGroupId ?? current.selectedGroupId ?? requestedGroupId;
2636
+ const sessionKey = buildSessionKey(modelId, effectiveGroupId);
2637
+ const session = this.sessions.get(sessionKey);
2581
2638
  if (session) {
2582
2639
  session.abortController.abort();
2583
- this.sessions.delete(modelId);
2640
+ this.sessions.delete(sessionKey);
2584
2641
  }
2585
- const current = await this.readSelectedModelState(modelId);
2642
+ const nextGroupsState = {
2643
+ ...current.groupsState,
2644
+ [effectiveGroupId]: LocalModelLifecycleGroupStateSchema.parse({
2645
+ ...current.groupsState[effectiveGroupId],
2646
+ groupId: effectiveGroupId,
2647
+ status: "paused",
2648
+ resumable: true,
2649
+ updatedAt: this.now()
2650
+ })
2651
+ };
2586
2652
  const nextState = LocalModelAssetStateSchema.parse({
2587
2653
  ...current,
2588
- status: "paused",
2589
- resumable: true,
2654
+ groupsState: nextGroupsState,
2590
2655
  updatedAt: this.now()
2591
2656
  });
2592
- await this.store.upsert(nextState);
2657
+ const projected = await this.refreshCachedState(nextState, effectiveGroupId, { revalidateDisk: true });
2658
+ await this.store.upsert(projected);
2593
2659
  this.emitLog({
2594
2660
  engineId: "local",
2595
2661
  modelId,
2596
- selectedGroupId: current.plan?.selectedGroupId,
2662
+ selectedGroupId: effectiveGroupId,
2663
+ groupId: effectiveGroupId,
2597
2664
  status: "paused",
2598
2665
  message: "Local model download paused.",
2599
- progress: nextState.progress,
2600
- bytesDownloaded: nextState.bytesDownloaded,
2601
- totalBytes: nextState.totalBytes,
2666
+ progress: projected.progress,
2667
+ bytesDownloaded: projected.bytesDownloaded,
2668
+ totalBytes: projected.totalBytes,
2602
2669
  resumable: true,
2603
- files: nextState.files,
2670
+ files: projected.files,
2604
2671
  updatedAt: this.now()
2605
2672
  });
2606
2673
  return { success: true };
2607
2674
  }
2608
- async deleteModel(modelId) {
2609
- this.sessions.get(modelId)?.abortController.abort();
2610
- this.sessions.delete(modelId);
2611
- const current = await this.readSelectedModelState(modelId);
2675
+ async deleteModel(modelId, groupId) {
2676
+ const requestedGroupId = groupId ?? await this.readSelectedGroupId();
2677
+ if (!requestedGroupId) {
2678
+ await this.store.remove(modelId);
2679
+ await this.profileManifestStore.remove(modelId);
2680
+ return { success: true };
2681
+ }
2682
+ const current = await this.readSelectedModelState(modelId, requestedGroupId);
2683
+ const effectiveGroupId = current.plan?.selectedGroupId ?? current.selectedGroupId ?? requestedGroupId;
2684
+ const sessionKey = buildSessionKey(modelId, effectiveGroupId);
2685
+ this.sessions.get(sessionKey)?.abortController.abort();
2686
+ this.sessions.delete(sessionKey);
2612
2687
  await this.store.upsert(LocalModelAssetStateSchema.parse({
2613
2688
  ...current,
2614
- status: "deleting",
2689
+ groupsState: {
2690
+ ...current.groupsState,
2691
+ [effectiveGroupId]: LocalModelLifecycleGroupStateSchema.parse({
2692
+ ...current.groupsState[effectiveGroupId],
2693
+ groupId: effectiveGroupId,
2694
+ status: "deleting",
2695
+ updatedAt: this.now()
2696
+ })
2697
+ },
2615
2698
  updatedAt: this.now()
2616
2699
  }));
2617
2700
  this.emitLog({
2618
2701
  engineId: "local",
2619
2702
  modelId,
2620
- selectedGroupId: current.plan?.selectedGroupId,
2703
+ selectedGroupId: effectiveGroupId,
2704
+ groupId: effectiveGroupId,
2621
2705
  status: "deleting",
2622
2706
  message: "Deleting local model files.",
2623
2707
  files: current.files,
2624
2708
  updatedAt: this.now()
2625
2709
  });
2626
- await mkdir(this.cacheDir, { recursive: true });
2627
- await rm(getTransformersLocalModelPath(this.cacheDir, modelId), {
2628
- recursive: true,
2629
- force: true
2630
- });
2631
- await rm(getTransformersFileCacheModelPath(this.cacheDir, modelId), {
2710
+ await rm(getLocalModelProfileGroupRoot(this.cacheDir, modelId, effectiveGroupId), {
2632
2711
  recursive: true,
2633
2712
  force: true
2634
2713
  });
2635
- await rm(getHubCacheRepoPath(this.cacheDir, modelId), {
2636
- recursive: true,
2637
- force: true
2638
- });
2639
- await this.store.remove(modelId);
2714
+ const persistedManifest = await this.profileManifestStore.read(modelId);
2715
+ const nextGroupsState = { ...current.groupsState };
2716
+ delete nextGroupsState[effectiveGroupId];
2717
+ const isPersistedManifestGroup = Boolean(persistedManifest?.groups[effectiveGroupId]);
2718
+ const nextManifest = isPersistedManifestGroup ? persistedManifest : current.profileManifest ? removeManifestGroup(current.profileManifest, effectiveGroupId) : void 0;
2719
+ const nextPlan = isPersistedManifestGroup ? void 0 : current.plan ? removePlanGroup(current.plan, effectiveGroupId) : void 0;
2720
+ const nextSelectedGroupId = current.selectedGroupId === effectiveGroupId ? void 0 : current.selectedGroupId;
2721
+ const nextState = await this.refreshCachedState(LocalModelAssetStateSchema.parse({
2722
+ ...current,
2723
+ selectedGroupId: nextSelectedGroupId,
2724
+ profileManifest: nextManifest,
2725
+ groupsState: nextGroupsState,
2726
+ plan: nextPlan,
2727
+ updatedAt: this.now()
2728
+ }), nextSelectedGroupId, { revalidateDisk: true });
2729
+ if (nextState.profileManifest) await this.profileManifestStore.upsert(nextState.profileManifest);
2730
+ else await this.profileManifestStore.remove(modelId);
2731
+ if (nextState.profileManifest || nextState.plan?.groups?.length) await this.store.upsert(nextState);
2732
+ else await this.store.remove(modelId);
2640
2733
  this.emitLog({
2641
2734
  engineId: "local",
2642
2735
  modelId,
2643
- selectedGroupId: current.plan?.selectedGroupId,
2736
+ selectedGroupId: effectiveGroupId,
2737
+ groupId: effectiveGroupId,
2644
2738
  status: "not-downloaded",
2645
2739
  message: "Local model files were removed.",
2646
2740
  progress: 0,
@@ -2651,6 +2745,49 @@ var LocalModelAssetService = class {
2651
2745
  });
2652
2746
  return { success: true };
2653
2747
  }
2748
+ async refreshProfiles(modelId) {
2749
+ const targetModelId = modelId ?? await this.readSelectedModel();
2750
+ const loadingState = LocalModelAssetStateSchema.parse({
2751
+ ...await this.readSelectedModelState(targetModelId),
2752
+ profileLoad: {
2753
+ status: "loading",
2754
+ message: "Loading local model profiles.",
2755
+ updatedAt: this.now()
2756
+ },
2757
+ updatedAt: this.now()
2758
+ });
2759
+ await this.store.upsert(loadingState);
2760
+ try {
2761
+ const manifest = await this.createProfileManifest(targetModelId);
2762
+ await this.profileManifestStore.upsert(manifest);
2763
+ const current = await this.readSelectedModelState(targetModelId);
2764
+ const nextState = await this.refreshCachedState(LocalModelAssetStateSchema.parse({
2765
+ ...current,
2766
+ profileManifest: manifest,
2767
+ profileLoad: {
2768
+ status: "ready",
2769
+ message: "Local model profiles are ready.",
2770
+ updatedAt: this.now()
2771
+ },
2772
+ updatedAt: this.now()
2773
+ }), void 0, { revalidateDisk: true });
2774
+ await this.store.upsert(nextState);
2775
+ return nextState;
2776
+ } catch (error) {
2777
+ const message = error instanceof Error ? error.message : "Unable to load local model profiles.";
2778
+ const failedState = LocalModelAssetStateSchema.parse({
2779
+ ...await this.readSelectedModelState(targetModelId),
2780
+ profileLoad: {
2781
+ status: "error",
2782
+ error: message,
2783
+ updatedAt: this.now()
2784
+ },
2785
+ updatedAt: this.now()
2786
+ });
2787
+ await this.store.upsert(failedState);
2788
+ throw error;
2789
+ }
2790
+ }
2654
2791
  async markSelectedModel(modelId) {
2655
2792
  const nextStates = (await this.store.readAll()).map((state) => LocalModelAssetStateSchema.parse({
2656
2793
  ...state,
@@ -2665,7 +2802,7 @@ var LocalModelAssetService = class {
2665
2802
  await this.store.writeAll(nextStates);
2666
2803
  }
2667
2804
  async waitForModelTask(modelId) {
2668
- await this.sessionTasks.get(modelId);
2805
+ await Promise.all([...this.sessionTasks.entries()].filter(([sessionKey]) => sessionKey.startsWith(`${modelId}:`)).map(([, task]) => task));
2669
2806
  }
2670
2807
  async close() {
2671
2808
  const sessions = [...this.sessions.values()];
@@ -2724,176 +2861,298 @@ var LocalModelAssetService = class {
2724
2861
  }));
2725
2862
  return [...remoteItems, ...localOnlyItems];
2726
2863
  }
2727
- async refreshCachedState(state, selectedGroupId) {
2728
- const requestedGroupId = selectedGroupId;
2729
- const session = this.sessions.get(state.modelId);
2730
- const selected = state.selected || state.modelId === await this.readSelectedModel();
2731
- if (state.status === "deleting") return LocalModelAssetStateSchema.parse({
2732
- ...state,
2733
- selected,
2734
- updatedAt: this.now()
2735
- });
2736
- if (state.status === "downloaded" && state.plan && (requestedGroupId === void 0 || requestedGroupId === state.plan.selectedGroupId)) {
2737
- const files$1 = (selectLocalDownloadGroup(state.plan, requestedGroupId ?? state.plan.selectedGroupId)?.files ?? state.plan.files).map((file) => ({
2738
- path: file.path,
2739
- sizeBytes: file.sizeBytes,
2740
- downloadedBytes: file.sizeBytes
2741
- }));
2742
- return LocalModelAssetStateSchema.parse({
2743
- ...state,
2744
- selected,
2745
- status: "downloaded",
2746
- progress: 1,
2747
- bytesDownloaded: state.totalBytes ?? state.plan.estimatedTotalBytes,
2748
- totalBytes: state.totalBytes ?? state.plan.estimatedTotalBytes,
2749
- resumable: false,
2750
- error: void 0,
2751
- files: files$1,
2752
- updatedAt: this.now(),
2753
- installedAt: state.installedAt ?? this.now()
2754
- });
2755
- }
2756
- const transformers = await this.getTransformersModule();
2757
- transformers.env.remoteHost = buildTransformersRemoteHost(await this.readHuggingFaceEndpoint());
2758
- const [plan, persistedSelectedGroupId] = await Promise.all([this.readPlan(state.modelId, transformers, requestedGroupId ?? state.plan?.selectedGroupId), this.readSelectedGroupId()]);
2759
- if (!plan && state.status !== "downloaded") return LocalModelAssetStateSchema.parse({
2760
- ...state,
2761
- selected,
2762
- plan: void 0
2864
+ async refreshCachedState(state, selectedGroupId, options = {}) {
2865
+ const [selectedModel, persistedSelectedGroupId] = await Promise.all([this.readSelectedModel(), this.readSelectedGroupId()]);
2866
+ const selected = state.selected || state.modelId === selectedModel;
2867
+ const selectedGroupIdFromSettings = selectedGroupId ?? persistedSelectedGroupId;
2868
+ const manifest = filterConcreteProfileManifest(state.profileManifest ?? await this.profileManifestStore.read(state.modelId) ?? void 0);
2869
+ const migrated = migrateLegacyStateToGroups(state, manifest, this.now());
2870
+ const manifestWithHistoricalGroups = mergeHistoricalGroupsIntoManifest({
2871
+ cacheDir: this.cacheDir,
2872
+ modelId: state.modelId,
2873
+ manifest,
2874
+ groupsState: migrated.groupsState,
2875
+ fallbackPlan: migrated.plan
2763
2876
  });
2764
- const effectivePlan = plan ?? {
2877
+ const selectedGroupIdForProjection = resolveManifestGroupId(manifestWithHistoricalGroups, selectedGroupIdFromSettings ?? migrated.selectedGroupId ?? migrated.plan?.selectedGroupId) ?? selectFirstManifestGroupId(manifestWithHistoricalGroups);
2878
+ const reconciledGroupsState = options.revalidateDisk ? await this.reconcileGroupsFromDisk({
2765
2879
  modelId: state.modelId,
2766
- estimatedTotalBytes: state.totalBytes,
2767
- files: state.files.map((file) => ({
2768
- path: file.path,
2769
- sizeBytes: file.sizeBytes,
2770
- required: true
2771
- })),
2772
- selectedGroupId: state.plan?.selectedGroupId,
2773
- groups: state.plan?.groups
2774
- };
2775
- const selectedGroup = selectLocalDownloadGroup(effectivePlan, requestedGroupId ?? state.plan?.selectedGroupId ?? persistedSelectedGroupId);
2776
- if (requestedGroupId && requestedGroupId !== effectivePlan.selectedGroupId && !selectedGroup) return LocalModelAssetStateSchema.parse({
2777
- ...state,
2778
- selected,
2779
- status: "not-downloaded",
2780
- progress: 0,
2781
- bytesDownloaded: 0,
2782
- totalBytes: void 0,
2783
- resumable: false,
2784
- files: [],
2785
- updatedAt: this.now()
2880
+ manifest: manifestWithHistoricalGroups,
2881
+ groupsState: migrated.groupsState
2882
+ }) : this.reconcileGroupsFromSnapshot({
2883
+ modelId: state.modelId,
2884
+ manifest: manifestWithHistoricalGroups,
2885
+ groupsState: migrated.groupsState
2786
2886
  });
2787
- const planFiles = selectedGroup?.files ?? effectivePlan.files;
2788
- const cacheStatus = await readLocalModelFileStatus({
2789
- cacheDir: this.cacheDir,
2887
+ const plan = buildPlanFromManifest({
2790
2888
  modelId: state.modelId,
2791
- files: planFiles.map((file) => file.path)
2792
- });
2793
- const sameRequestedGroup = requestedGroupId === void 0 || requestedGroupId === state.plan?.selectedGroupId;
2794
- const cachedFileSet = new Set(cacheStatus.files.filter((file) => file.cached).map((file) => file.file));
2795
- const runtimeAllCached = cacheStatus.allCached;
2796
- const files = planFiles.map((file) => {
2797
- const cached = cachedFileSet.has(file.path);
2798
- const existingDownloadedBytes = state.files.find((entry) => entry.path === file.path)?.downloadedBytes ?? 0;
2799
- const downloadedBytes = file.sizeBytes === void 0 ? cached ? existingDownloadedBytes : existingDownloadedBytes : cached ? file.sizeBytes : Math.min(existingDownloadedBytes, file.sizeBytes);
2889
+ manifest: manifestWithHistoricalGroups,
2890
+ groupsState: reconciledGroupsState,
2891
+ selectedGroupId: selectedGroupIdForProjection
2892
+ });
2893
+ const selectedPlanGroup = selectLocalDownloadGroup(plan, selectedGroupIdForProjection);
2894
+ const selectedGroupState = selectedPlanGroup && reconciledGroupsState[selectedPlanGroup.id] ? reconciledGroupsState[selectedPlanGroup.id] : void 0;
2895
+ const files = selectedPlanGroup?.files.map((file) => {
2896
+ const stateFile = selectedGroupState?.files.find((entry) => entry.path === file.path);
2800
2897
  return {
2801
2898
  path: file.path,
2802
2899
  sizeBytes: file.sizeBytes,
2803
- downloadedBytes
2900
+ downloadedBytes: stateFile?.downloadedBytes ?? 0
2804
2901
  };
2805
- });
2806
- const detectedBytesDownloaded = sumDownloadedBytes(files);
2807
- const detectedProgress = effectivePlan.estimatedTotalBytes !== void 0 && effectivePlan.estimatedTotalBytes > 0 ? detectedBytesDownloaded / effectivePlan.estimatedTotalBytes : runtimeAllCached ? 1 : void 0;
2808
- const progress = cacheStatus.allCached ? 1 : session ? state.progress : detectedProgress;
2809
- const hasPartialCache = !runtimeAllCached && detectedBytesDownloaded > 0;
2902
+ }) ?? [];
2903
+ const status = selectedGroupState?.status ?? "not-downloaded";
2810
2904
  return LocalModelAssetStateSchema.parse({
2811
- ...state,
2905
+ ...migrated,
2812
2906
  selected,
2813
- plan: effectivePlan,
2814
- status: runtimeAllCached ? "downloaded" : session ? state.status : sameRequestedGroup && state.status === "paused" ? "paused" : sameRequestedGroup && state.status === "error" ? "error" : hasPartialCache ? "paused" : "not-downloaded",
2815
- progress: progress === void 0 ? void 0 : Math.max(0, Math.min(1, progress)),
2816
- totalBytes: effectivePlan.estimatedTotalBytes ?? state.totalBytes,
2817
- bytesDownloaded: session ? state.bytesDownloaded : detectedBytesDownloaded,
2818
- error: runtimeAllCached ? void 0 : state.error,
2819
- resumable: runtimeAllCached ? false : sameRequestedGroup && state.status === "paused" || sameRequestedGroup && state.status === "error" || hasPartialCache || progress !== void 0 && progress > 0 && progress < 1,
2907
+ selectedGroupId: selectedGroupIdForProjection,
2908
+ profileManifest: manifestWithHistoricalGroups,
2909
+ groupsState: reconciledGroupsState,
2910
+ plan: plan ?? void 0,
2911
+ status,
2912
+ progress: selectedGroupState?.progress,
2913
+ totalBytes: selectedGroupState?.totalBytes ?? selectedPlanGroup?.estimatedTotalBytes,
2914
+ bytesDownloaded: selectedGroupState?.bytesDownloaded,
2915
+ error: selectedGroupState?.error,
2916
+ resumable: selectedGroupState?.resumable ?? false,
2820
2917
  files,
2821
2918
  updatedAt: this.now(),
2822
- installedAt: runtimeAllCached ? state.installedAt ?? this.now() : state.installedAt
2919
+ installedAt: selectedGroupState?.installedAt ?? state.installedAt
2823
2920
  });
2824
2921
  }
2825
- async runDownload(modelId, targetStatus, messagePrefix, selectedGroupId) {
2826
- const existing = this.sessions.get(modelId);
2922
+ reconcileGroupsFromSnapshot(input) {
2923
+ if (!input.manifest) return input.groupsState;
2924
+ const nextGroupsState = { ...input.groupsState };
2925
+ for (const groupId of input.manifest.groupOrder) {
2926
+ const manifestGroup = input.manifest.groups[groupId];
2927
+ if (!manifestGroup) continue;
2928
+ const current = nextGroupsState[groupId];
2929
+ const files = reconcileGroupFilesFromSnapshot({
2930
+ manifestGroup,
2931
+ currentFiles: current?.files ?? [],
2932
+ currentStatus: current?.status ?? "not-downloaded"
2933
+ });
2934
+ const bytesDownloaded = sumDownloadedBytes(files);
2935
+ const totalBytes = manifestGroup.estimatedTotalBytes;
2936
+ const status = current?.status ?? "not-downloaded";
2937
+ nextGroupsState[groupId] = LocalModelLifecycleGroupStateSchema.parse({
2938
+ ...current,
2939
+ groupId,
2940
+ baseGroupId: manifestGroup.baseGroupId,
2941
+ status,
2942
+ rootDir: manifestGroup.rootDir,
2943
+ bytesDownloaded,
2944
+ totalBytes,
2945
+ progress: totalBytes && totalBytes > 0 ? Math.max(0, Math.min(1, bytesDownloaded / totalBytes)) : current?.progress,
2946
+ resumable: current?.resumable ?? (status === "paused" || status === "error" || status === "downloading"),
2947
+ error: current?.error,
2948
+ installedAt: current?.installedAt,
2949
+ updatedAt: current?.updatedAt ?? this.now(),
2950
+ files
2951
+ });
2952
+ }
2953
+ return nextGroupsState;
2954
+ }
2955
+ async reconcileGroupsFromDisk(input) {
2956
+ if (!input.manifest) return input.groupsState;
2957
+ const nextGroupsState = { ...input.groupsState };
2958
+ for (const groupId of input.manifest.groupOrder) {
2959
+ const manifestGroup = input.manifest.groups[groupId];
2960
+ if (!manifestGroup) continue;
2961
+ const current = nextGroupsState[groupId];
2962
+ if (isActiveDownloadStatus(current?.status ?? "not-downloaded")) {
2963
+ nextGroupsState[groupId] = LocalModelLifecycleGroupStateSchema.parse({
2964
+ ...current,
2965
+ groupId,
2966
+ baseGroupId: manifestGroup.baseGroupId,
2967
+ rootDir: manifestGroup.rootDir,
2968
+ totalBytes: manifestGroup.estimatedTotalBytes,
2969
+ files: reconcileGroupFiles({
2970
+ manifestGroup,
2971
+ currentFiles: current?.files ?? []
2972
+ })
2973
+ });
2974
+ continue;
2975
+ }
2976
+ const files = await reconcileGroupFilesFromDisk({
2977
+ rootDir: manifestGroup.rootDir,
2978
+ manifestGroup,
2979
+ currentFiles: current?.files ?? []
2980
+ });
2981
+ const bytesDownloaded = sumDownloadedBytes(files);
2982
+ const totalBytes = manifestGroup.estimatedTotalBytes;
2983
+ const allComplete = files.length > 0 && files.every((file) => file.sizeBytes !== void 0 && (file.downloadedBytes ?? 0) >= file.sizeBytes && file.status === "downloaded");
2984
+ const hasPartial = files.some((file) => (file.downloadedBytes ?? 0) > 0);
2985
+ const status = allComplete ? "downloaded" : current?.status === "error" ? "error" : current?.status === "paused" ? "paused" : hasPartial ? "paused" : "not-downloaded";
2986
+ nextGroupsState[groupId] = LocalModelLifecycleGroupStateSchema.parse({
2987
+ ...current,
2988
+ groupId,
2989
+ baseGroupId: manifestGroup.baseGroupId,
2990
+ status,
2991
+ rootDir: manifestGroup.rootDir,
2992
+ bytesDownloaded,
2993
+ totalBytes,
2994
+ progress: totalBytes && totalBytes > 0 ? Math.max(0, Math.min(1, bytesDownloaded / totalBytes)) : void 0,
2995
+ resumable: status === "paused" || status === "error",
2996
+ error: status === "error" ? current?.error : void 0,
2997
+ installedAt: status === "downloaded" ? current?.installedAt ?? this.now() : current?.installedAt,
2998
+ updatedAt: this.now(),
2999
+ files
3000
+ });
3001
+ }
3002
+ return nextGroupsState;
3003
+ }
3004
+ async runDownload(modelId, targetStatus, messagePrefix, groupId) {
3005
+ const effectiveGroupId = groupId ?? await this.readSelectedGroupId();
3006
+ if (!effectiveGroupId) throw new Error("No local model profile is selected.");
3007
+ const manifest = await this.ensureProfileManifest(modelId);
3008
+ const resolvedGroupId = resolveManifestGroupId(manifest, effectiveGroupId);
3009
+ if (!resolvedGroupId) throw new Error("No concrete local model download plan is available.");
3010
+ const sessionKey = buildSessionKey(modelId, resolvedGroupId);
3011
+ const existing = this.sessions.get(sessionKey);
2827
3012
  if (existing) return { sessionId: existing.sessionId };
2828
- const sessionId = `local-model-${sanitizeId(modelId)}-${this.now()}`;
3013
+ const sessionId = `local-model-${sanitizeId(modelId)}-${sanitizeId(resolvedGroupId)}-${this.now()}`;
2829
3014
  const abortController = new AbortController();
2830
- this.sessions.set(modelId, {
3015
+ this.sessions.set(sessionKey, {
2831
3016
  modelId,
2832
3017
  sessionId,
2833
3018
  abortController,
2834
- selectedGroupId
3019
+ groupId: resolvedGroupId
2835
3020
  });
2836
- const current = await this.readSelectedModelState(modelId);
2837
- const transformers = await this.getTransformersModule();
2838
- const plan = await this.readPlan(modelId, transformers, selectedGroupId ?? current.plan?.selectedGroupId);
2839
- if (!plan || plan.files.length === 0 || plan.estimatedTotalBytes === void 0) {
2840
- this.sessions.delete(modelId);
3021
+ const current = await this.readSelectedModelState(modelId, resolvedGroupId);
3022
+ const manifestGroup = manifest.groups[resolvedGroupId];
3023
+ if (!manifestGroup || manifestGroup.files.length === 0 || manifestGroup.estimatedTotalBytes === void 0) {
3024
+ this.sessions.delete(sessionKey);
2841
3025
  throw new Error("No concrete local model download plan is available.");
2842
3026
  }
2843
- const totalBytes = plan.estimatedTotalBytes;
2844
- const resumedFiles = buildDownloadStateFiles({
2845
- planFiles: plan.files,
2846
- currentFiles: current.files
3027
+ const totalBytes = manifestGroup.estimatedTotalBytes;
3028
+ const currentGroup = current.groupsState[resolvedGroupId];
3029
+ const resumedFiles = await reconcileGroupFilesFromDisk({
3030
+ rootDir: manifestGroup.rootDir,
3031
+ manifestGroup,
3032
+ currentFiles: currentGroup?.files ?? []
2847
3033
  });
2848
3034
  const resumedBytesDownloaded = sumDownloadedBytes(resumedFiles);
2849
3035
  const nextState = LocalModelAssetStateSchema.parse({
2850
3036
  ...current,
2851
3037
  modelId,
2852
- plan,
2853
- status: targetStatus,
2854
3038
  selected: true,
2855
- bytesDownloaded: resumedBytesDownloaded,
2856
- progress: totalBytes > 0 ? resumedBytesDownloaded / totalBytes : current.progress,
2857
- totalBytes,
2858
- resumable: true,
2859
- files: resumedFiles,
3039
+ profileManifest: manifest,
3040
+ groupsState: {
3041
+ ...current.groupsState,
3042
+ [resolvedGroupId]: LocalModelLifecycleGroupStateSchema.parse({
3043
+ ...currentGroup,
3044
+ groupId: resolvedGroupId,
3045
+ baseGroupId: manifestGroup.baseGroupId,
3046
+ status: targetStatus,
3047
+ rootDir: manifestGroup.rootDir,
3048
+ bytesDownloaded: resumedBytesDownloaded,
3049
+ progress: totalBytes > 0 ? resumedBytesDownloaded / totalBytes : currentGroup?.progress,
3050
+ totalBytes,
3051
+ resumable: true,
3052
+ files: resumedFiles,
3053
+ updatedAt: this.now()
3054
+ })
3055
+ },
2860
3056
  updatedAt: this.now()
2861
3057
  });
2862
- await this.store.upsert(nextState);
3058
+ const projected = await this.refreshCachedState(nextState, resolvedGroupId, { revalidateDisk: true });
3059
+ await this.store.upsert(projected);
2863
3060
  this.emitLog({
2864
3061
  engineId: "local",
2865
3062
  modelId,
2866
- selectedGroupId: nextState.plan?.selectedGroupId,
3063
+ selectedGroupId: resolvedGroupId,
3064
+ groupId: resolvedGroupId,
2867
3065
  status: targetStatus,
2868
3066
  message: `${messagePrefix} ${modelId}.`,
2869
- progress: nextState.progress,
2870
- bytesDownloaded: nextState.bytesDownloaded,
3067
+ progress: projected.progress,
3068
+ bytesDownloaded: projected.bytesDownloaded,
2871
3069
  totalBytes,
2872
3070
  sessionId,
2873
3071
  resumable: true,
2874
- files: nextState.files,
3072
+ files: projected.files,
2875
3073
  updatedAt: this.now()
2876
3074
  });
2877
- const task = this.performDownload(modelId, sessionId, abortController.signal, nextState).catch((error) => this.finishDownload(modelId, sessionId, false, error instanceof Error ? error.message : String(error))).finally(() => {
2878
- if (this.sessionTasks.get(modelId) === task) this.sessionTasks.delete(modelId);
3075
+ const task = this.performDownload(modelId, resolvedGroupId, sessionId, abortController.signal).catch((error) => this.finishDownload(modelId, resolvedGroupId, sessionId, false, error instanceof Error ? error.message : String(error))).finally(() => {
3076
+ if (this.sessionTasks.get(sessionKey) === task) this.sessionTasks.delete(sessionKey);
2879
3077
  });
2880
- this.sessionTasks.set(modelId, task);
3078
+ this.sessionTasks.set(sessionKey, task);
2881
3079
  return { sessionId };
2882
3080
  }
2883
- async performDownload(modelId, sessionId, signal, state) {
2884
- const transformers = await this.getTransformersModule();
2885
- await configureTransformersRuntime(transformers, this.cacheDir);
2886
- transformers.env.remoteHost = buildTransformersRemoteHost(await this.readHuggingFaceEndpoint());
2887
- const selectedGroup = selectLocalDownloadGroup(state.plan ?? null, state.plan?.selectedGroupId);
2888
- const files = selectedGroup?.files ?? state.plan?.files ?? [];
2889
- const totalBytes = selectedGroup?.estimatedTotalBytes ?? state.plan?.estimatedTotalBytes;
3081
+ async ensureProfileManifest(modelId) {
3082
+ const existing = await this.profileManifestStore.read(modelId);
3083
+ if (existing) return existing;
3084
+ const manifest = await this.createProfileManifest(modelId);
3085
+ await this.profileManifestStore.upsert(manifest);
3086
+ return manifest;
3087
+ }
3088
+ async createProfileManifest(modelId) {
3089
+ const hfEndpoint = await this.readHuggingFaceEndpoint();
3090
+ const snapshot = await readLocalModelRepositorySnapshot({
3091
+ modelId,
3092
+ hfEndpoint,
3093
+ fetchCacheStore: this.fetchCacheStore
3094
+ });
3095
+ const basePlan = buildLocalDownloadPlanFromRepositoryFiles({
3096
+ modelId,
3097
+ files: snapshot.files.map((file) => ({
3098
+ ...file,
3099
+ revision: snapshot.commitHash
3100
+ }))
3101
+ });
3102
+ if (!basePlan?.groups?.length) throw new Error(`No recognizable local model profiles were found for ${modelId}.`);
3103
+ const groupsEntries = basePlan.groups.flatMap((group) => {
3104
+ if (!group.selectable || group.estimatedTotalBytes === void 0) return [];
3105
+ const groupId = buildVersionedGroupId(group.id, snapshot.shortCommitHash);
3106
+ const rootDir = getLocalModelProfileGroupRoot(this.cacheDir, modelId, groupId);
3107
+ return [[groupId, {
3108
+ id: groupId,
3109
+ baseGroupId: group.id,
3110
+ label: group.label,
3111
+ displayLabel: group.label,
3112
+ description: group.description,
3113
+ profile: group.profile,
3114
+ dtype: group.dtype,
3115
+ commitHash: snapshot.commitHash,
3116
+ shortCommitHash: snapshot.shortCommitHash,
3117
+ rootDir,
3118
+ estimatedTotalBytes: group.estimatedTotalBytes,
3119
+ selectable: group.selectable,
3120
+ files: group.files.map((file) => ({
3121
+ ...file,
3122
+ revision: snapshot.commitHash,
3123
+ sourceUrl: file.sourceUrl ?? `${normalizeHuggingFaceEndpoint(hfEndpoint)}/${modelId}/resolve/${snapshot.commitHash}/${file.path}`
3124
+ }))
3125
+ }]];
3126
+ });
3127
+ if (groupsEntries.length === 0) throw new Error(`No selectable local model profiles were found for ${modelId}.`);
3128
+ return LocalModelProfileManifestSchema.parse({
3129
+ modelId,
3130
+ source: "huggingface",
3131
+ endpoint: normalizeHuggingFaceEndpoint(hfEndpoint),
3132
+ revision: snapshot.revision,
3133
+ commitHash: snapshot.commitHash,
3134
+ shortCommitHash: snapshot.shortCommitHash,
3135
+ fetchedAt: this.now(),
3136
+ updatedAt: this.now(),
3137
+ raw: snapshot.raw,
3138
+ groups: Object.fromEntries(groupsEntries),
3139
+ groupOrder: groupsEntries.map(([groupId]) => groupId)
3140
+ });
3141
+ }
3142
+ async performDownload(modelId, groupId, sessionId, signal) {
3143
+ const manifestGroup = (await this.ensureProfileManifest(modelId)).groups[groupId];
3144
+ if (!manifestGroup) throw new Error(`Unknown local model profile: ${groupId}.`);
3145
+ const files = manifestGroup.files;
3146
+ const totalBytes = manifestGroup.estimatedTotalBytes;
2890
3147
  const hfEndpoint = normalizeHuggingFaceEndpoint(await this.readHuggingFaceEndpoint());
2891
- const downloadedFiles = buildDownloadStateFiles({
2892
- planFiles: files,
2893
- currentFiles: state.files
3148
+ const currentGroup = (await this.readSelectedModelState(modelId, groupId)).groupsState[groupId];
3149
+ const downloadedFiles = await reconcileGroupFilesFromDisk({
3150
+ rootDir: manifestGroup.rootDir,
3151
+ manifestGroup,
3152
+ currentFiles: currentGroup?.files ?? []
2894
3153
  });
2895
3154
  let bytesDownloaded = sumDownloadedBytes(downloadedFiles);
2896
- if (files.length === 0) throw new Error("No concrete local model download files were selected.");
3155
+ if (files.length === 0 || totalBytes === void 0) throw new Error("No concrete local model download files were selected.");
2897
3156
  for (const [fileIndex, file] of files.entries()) {
2898
3157
  throwIfAborted(signal);
2899
3158
  const previousFileBytes = downloadedFiles[fileIndex]?.downloadedBytes ?? 0;
@@ -2901,12 +3160,14 @@ var LocalModelAssetService = class {
2901
3160
  downloadedFiles[fileIndex] = {
2902
3161
  path: file.path,
2903
3162
  sizeBytes: file.sizeBytes,
2904
- downloadedBytes: previousFileBytes
3163
+ downloadedBytes: previousFileBytes,
3164
+ required: file.required,
3165
+ status: previousFileBytes > 0 ? "paused" : "not-downloaded"
2905
3166
  };
2906
3167
  await this.emitDownloadProgress({
2907
3168
  modelId,
3169
+ groupId,
2908
3170
  sessionId,
2909
- state,
2910
3171
  message: `Downloading ${file.path}.`,
2911
3172
  totalBytes,
2912
3173
  bytesDownloaded,
@@ -2919,7 +3180,10 @@ var LocalModelAssetService = class {
2919
3180
  },
2920
3181
  path: file.path,
2921
3182
  cacheDir: this.cacheDir,
3183
+ targetPath: join(manifestGroup.rootDir, file.path),
2922
3184
  hubUrl: hfEndpoint,
3185
+ revision: manifestGroup.commitHash,
3186
+ etag: file.etag,
2923
3187
  expectedSizeBytes: file.sizeBytes,
2924
3188
  retryPolicy: this.networkRetryPolicy,
2925
3189
  fetch: createAbortableFetch(signal),
@@ -2930,12 +3194,14 @@ var LocalModelAssetService = class {
2930
3194
  downloadedFiles[fileIndex] = {
2931
3195
  path: file.path,
2932
3196
  sizeBytes: file.sizeBytes,
2933
- downloadedBytes: boundedFileBytes
3197
+ downloadedBytes: boundedFileBytes,
3198
+ required: file.required,
3199
+ status: boundedFileBytes >= (file.sizeBytes ?? Number.POSITIVE_INFINITY) ? "downloaded" : "downloading"
2934
3200
  };
2935
3201
  await this.emitDownloadProgress({
2936
3202
  modelId,
3203
+ groupId,
2937
3204
  sessionId,
2938
- state,
2939
3205
  message: `Downloading ${file.path}.`,
2940
3206
  totalBytes,
2941
3207
  bytesDownloaded: bytesDownloaded - previousFileBytes + boundedFileBytes,
@@ -2946,8 +3212,8 @@ var LocalModelAssetService = class {
2946
3212
  const retryTarget = phase === "metadata" ? `metadata for ${file.path}` : `${file.path}`;
2947
3213
  await this.emitDownloadProgress({
2948
3214
  modelId,
3215
+ groupId,
2949
3216
  sessionId,
2950
- state,
2951
3217
  message: `Connection interrupted while downloading ${retryTarget}. Retrying automatically in ${formatDuration(retryDelayMs)}.`,
2952
3218
  totalBytes,
2953
3219
  bytesDownloaded: bytesDownloaded - previousFileBytes + (downloadedFiles[fileIndex]?.downloadedBytes ?? 0),
@@ -2958,6 +3224,7 @@ var LocalModelAssetService = class {
2958
3224
  await mirrorHubCacheFileForTransformers({
2959
3225
  cacheDir: this.cacheDir,
2960
3226
  modelId,
3227
+ profileRoot: manifestGroup.rootDir,
2961
3228
  filePath: file.path,
2962
3229
  cachedPath
2963
3230
  });
@@ -2967,95 +3234,123 @@ var LocalModelAssetService = class {
2967
3234
  downloadedFiles[fileIndex] = {
2968
3235
  path: file.path,
2969
3236
  sizeBytes: file.sizeBytes,
2970
- downloadedBytes: file.sizeBytes
3237
+ downloadedBytes: file.sizeBytes,
3238
+ required: file.required,
3239
+ status: "downloaded"
2971
3240
  };
2972
3241
  await this.emitDownloadProgress({
2973
3242
  modelId,
3243
+ groupId,
2974
3244
  sessionId,
2975
- state,
2976
3245
  message: `Downloaded ${file.path}.`,
2977
3246
  totalBytes,
2978
3247
  bytesDownloaded,
2979
3248
  files: downloadedFiles
2980
3249
  });
2981
3250
  }
2982
- await this.finishDownload(modelId, sessionId, true, `Local model ${modelId} is ready.`);
3251
+ await this.finishDownload(modelId, groupId, sessionId, true, `Local model ${modelId} is ready.`);
2983
3252
  }
2984
3253
  async emitDownloadProgress(input) {
2985
- if (!this.isActiveSession(input.modelId, input.sessionId)) return;
3254
+ if (!this.isActiveSession(input.modelId, input.groupId, input.sessionId)) return;
2986
3255
  const progress = input.totalBytes && input.totalBytes > 0 ? Math.max(0, Math.min(1, input.bytesDownloaded / input.totalBytes)) : void 0;
3256
+ const current = await this.readSelectedModelState(input.modelId, input.groupId);
3257
+ const currentGroup = current.groupsState[input.groupId];
2987
3258
  const nextState = LocalModelAssetStateSchema.parse({
2988
- ...input.state,
2989
- status: "downloading",
2990
- progress,
2991
- bytesDownloaded: input.bytesDownloaded,
2992
- totalBytes: input.totalBytes,
2993
- files: input.files,
2994
- updatedAt: this.now(),
2995
- resumable: true
3259
+ ...current,
3260
+ groupsState: {
3261
+ ...current.groupsState,
3262
+ [input.groupId]: LocalModelLifecycleGroupStateSchema.parse({
3263
+ ...currentGroup,
3264
+ groupId: input.groupId,
3265
+ status: "downloading",
3266
+ bytesDownloaded: input.bytesDownloaded,
3267
+ totalBytes: input.totalBytes,
3268
+ progress,
3269
+ resumable: true,
3270
+ files: input.files,
3271
+ updatedAt: this.now()
3272
+ })
3273
+ },
3274
+ updatedAt: this.now()
2996
3275
  });
2997
- await this.store.upsert(nextState);
3276
+ const projected = await this.refreshCachedState(nextState, input.groupId, { revalidateDisk: true });
3277
+ await this.store.upsert(projected);
2998
3278
  this.emitLog({
2999
3279
  engineId: "local",
3000
3280
  modelId: input.modelId,
3001
- selectedGroupId: input.state.plan?.selectedGroupId,
3281
+ selectedGroupId: input.groupId,
3282
+ groupId: input.groupId,
3002
3283
  status: "downloading",
3003
3284
  message: input.message,
3004
3285
  progress,
3005
3286
  bytesDownloaded: input.bytesDownloaded,
3006
3287
  totalBytes: input.totalBytes,
3007
- files: input.files,
3288
+ files: input.files.map((file) => ({
3289
+ path: file.path,
3290
+ sizeBytes: file.sizeBytes,
3291
+ downloadedBytes: file.downloadedBytes
3292
+ })),
3008
3293
  sessionId: input.sessionId,
3009
3294
  resumable: true,
3010
3295
  updatedAt: this.now()
3011
3296
  });
3012
3297
  }
3013
- async finishDownload(modelId, sessionId, success, message) {
3014
- if (!this.isActiveSession(modelId, sessionId)) return;
3015
- const current = await this.readSelectedModelState(modelId);
3298
+ async finishDownload(modelId, groupId, sessionId, success, message) {
3299
+ if (!this.isActiveSession(modelId, groupId, sessionId)) return;
3300
+ const sessionKey = buildSessionKey(modelId, groupId);
3301
+ const current = await this.readSelectedModelState(modelId, groupId);
3302
+ const currentGroup = current.groupsState[groupId];
3303
+ const totalBytes = currentGroup?.totalBytes ?? current.totalBytes;
3304
+ const files = success ? current.files.map((file) => LocalModelLifecycleFileStateSchema.parse({
3305
+ ...file,
3306
+ required: true,
3307
+ downloadedBytes: file.sizeBytes,
3308
+ status: "downloaded",
3309
+ updatedAt: this.now()
3310
+ })) : (currentGroup?.files ?? []).map((file) => LocalModelLifecycleFileStateSchema.parse({
3311
+ ...file,
3312
+ status: file.status === "downloaded" ? "downloaded" : "paused",
3313
+ updatedAt: this.now()
3314
+ }));
3016
3315
  const nextState = LocalModelAssetStateSchema.parse({
3017
3316
  ...current,
3018
- status: success ? "downloaded" : "error",
3019
- progress: success ? 1 : current.progress,
3020
- bytesDownloaded: success ? current.totalBytes ?? current.bytesDownloaded : current.bytesDownloaded,
3021
- totalBytes: current.totalBytes,
3022
- installedAt: success ? this.now() : current.installedAt,
3023
- updatedAt: this.now(),
3024
- error: success ? void 0 : message,
3025
- resumable: !success
3317
+ groupsState: {
3318
+ ...current.groupsState,
3319
+ [groupId]: LocalModelLifecycleGroupStateSchema.parse({
3320
+ ...currentGroup,
3321
+ groupId,
3322
+ status: success ? "downloaded" : "error",
3323
+ progress: success ? 1 : current.progress,
3324
+ bytesDownloaded: success ? totalBytes : current.bytesDownloaded,
3325
+ totalBytes,
3326
+ installedAt: success ? this.now() : currentGroup?.installedAt,
3327
+ updatedAt: this.now(),
3328
+ error: success ? void 0 : message,
3329
+ resumable: !success,
3330
+ files
3331
+ })
3332
+ },
3333
+ updatedAt: this.now()
3026
3334
  });
3027
- await this.store.upsert(nextState);
3028
- this.sessions.delete(modelId);
3335
+ const projected = await this.refreshCachedState(nextState, groupId, { revalidateDisk: true });
3336
+ await this.store.upsert(projected);
3337
+ this.sessions.delete(sessionKey);
3029
3338
  this.emitLog({
3030
3339
  engineId: "local",
3031
3340
  modelId,
3032
- selectedGroupId: nextState.plan?.selectedGroupId,
3033
- status: nextState.status,
3341
+ selectedGroupId: groupId,
3342
+ groupId,
3343
+ status: projected.status,
3034
3344
  message,
3035
- progress: nextState.progress,
3036
- bytesDownloaded: nextState.bytesDownloaded,
3037
- totalBytes: nextState.totalBytes,
3345
+ progress: projected.progress,
3346
+ bytesDownloaded: projected.bytesDownloaded,
3347
+ totalBytes: projected.totalBytes,
3038
3348
  sessionId,
3039
- resumable: nextState.resumable,
3040
- files: nextState.files,
3349
+ resumable: projected.resumable,
3350
+ files: projected.files,
3041
3351
  updatedAt: this.now()
3042
3352
  });
3043
3353
  }
3044
- async readPlan(modelId, transformers, selectedGroupId) {
3045
- return resolveLocalModelRuntimePlan({
3046
- modelId,
3047
- transformers,
3048
- cacheDir: this.cacheDir,
3049
- selectedGroupId: selectedGroupId ?? await this.readSelectedGroupId(),
3050
- hfEndpoint: await this.readHuggingFaceEndpoint(),
3051
- fetchCacheStore: this.fetchCacheStore
3052
- }).catch(() => null);
3053
- }
3054
- async readPlanForState(modelId, selectedGroupId) {
3055
- const transformers = await this.getTransformersModule();
3056
- transformers.env.remoteHost = buildTransformersRemoteHost(await this.readHuggingFaceEndpoint());
3057
- return this.readPlan(modelId, transformers, selectedGroupId);
3058
- }
3059
3354
  async readSelectedModel() {
3060
3355
  return (await this.options.globalSettingsManager.readSettings()).translationEngines.local.model;
3061
3356
  }
@@ -3065,8 +3360,8 @@ var LocalModelAssetService = class {
3065
3360
  async readHuggingFaceEndpoint() {
3066
3361
  return (await this.options.globalSettingsManager.readSettings()).translationEngines.local.hfEndpoint;
3067
3362
  }
3068
- isActiveSession(modelId, sessionId) {
3069
- return this.sessions.get(modelId)?.sessionId === sessionId;
3363
+ isActiveSession(modelId, groupId, sessionId) {
3364
+ return this.sessions.get(buildSessionKey(modelId, groupId))?.sessionId === sessionId;
3070
3365
  }
3071
3366
  emitLog(log) {
3072
3367
  this.logs.set(log.modelId, log);
@@ -3080,17 +3375,279 @@ var LocalModelAssetService = class {
3080
3375
  return import("@huggingface/transformers");
3081
3376
  }
3082
3377
  };
3083
- function buildDownloadStateFiles(input) {
3378
+ function buildSessionKey(modelId, groupId) {
3379
+ return `${modelId}::${groupId}`;
3380
+ }
3381
+ function buildVersionedGroupId(baseGroupId, shortCommitHash) {
3382
+ return `${sanitizeId(baseGroupId)}-${sanitizeId(shortCommitHash)}`;
3383
+ }
3384
+ function selectFirstManifestGroupId(manifest) {
3385
+ return manifest?.groupOrder.find((groupId) => manifest.groups[groupId]?.selectable);
3386
+ }
3387
+ function resolveManifestGroupId(manifest, requestedGroupId) {
3388
+ if (!manifest || !requestedGroupId) return requestedGroupId;
3389
+ if (manifest.groups[requestedGroupId]?.selectable) return requestedGroupId;
3390
+ return manifest.groupOrder.find((groupId) => {
3391
+ const group = manifest.groups[groupId];
3392
+ return group?.selectable && group.baseGroupId === requestedGroupId;
3393
+ });
3394
+ }
3395
+ function removeManifestGroup(manifest, groupId) {
3396
+ const groups = { ...manifest.groups };
3397
+ delete groups[groupId];
3398
+ const groupOrder = manifest.groupOrder.filter((id) => id !== groupId);
3399
+ if (groupOrder.length === 0) return void 0;
3400
+ return LocalModelProfileManifestSchema.parse({
3401
+ ...manifest,
3402
+ groups,
3403
+ groupOrder
3404
+ });
3405
+ }
3406
+ function removePlanGroup(plan, groupId) {
3407
+ const groups = plan.groups?.filter((group) => group.id !== groupId);
3408
+ if (!groups?.length) return void 0;
3409
+ const selectedGroup = groups.find((group) => group.selected) ?? groups[0];
3410
+ return {
3411
+ ...plan,
3412
+ selectedGroupId: selectedGroup?.id,
3413
+ estimatedTotalBytes: selectedGroup?.estimatedTotalBytes,
3414
+ files: selectedGroup?.files ?? [],
3415
+ groups: groups.map((group) => ({
3416
+ ...group,
3417
+ selected: group.id === selectedGroup?.id
3418
+ }))
3419
+ };
3420
+ }
3421
+ function migrateLegacyStateToGroups(state, manifest, now) {
3422
+ const selectedGroupId = state.selectedGroupId ?? state.plan?.selectedGroupId;
3423
+ const groupsState = { ...state.groupsState };
3424
+ for (const group of manifest ? [] : state.plan?.groups ?? []) {
3425
+ if (groupsState[group.id] || !group.status || group.status === "not-downloaded") continue;
3426
+ const groupStatus = group.status;
3427
+ const manifestGroup = manifest?.groups[group.id];
3428
+ const groupFiles = group.id === selectedGroupId && state.files.length > 0 ? state.files.map((file) => ({
3429
+ ...file,
3430
+ required: true,
3431
+ status: file.sizeBytes !== void 0 && (file.downloadedBytes ?? 0) >= file.sizeBytes ? "downloaded" : normalizeLiveStatusForStoredState(groupStatus)
3432
+ })) : group.files.map((file) => ({
3433
+ ...file,
3434
+ downloadedBytes: groupStatus === "downloaded" ? file.sizeBytes : 0,
3435
+ status: normalizeLiveStatusForStoredState(groupStatus)
3436
+ }));
3437
+ const bytesDownloaded = sumDownloadedBytes(groupFiles);
3438
+ const totalBytes = group.estimatedTotalBytes;
3439
+ groupsState[group.id] = LocalModelLifecycleGroupStateSchema.parse({
3440
+ groupId: group.id,
3441
+ baseGroupId: group.baseGroupId ?? manifestGroup?.baseGroupId ?? group.id,
3442
+ status: normalizeLiveStatusForStoredState(groupStatus),
3443
+ rootDir: group.rootDir ?? manifestGroup?.rootDir,
3444
+ bytesDownloaded,
3445
+ totalBytes,
3446
+ progress: totalBytes && totalBytes > 0 ? Math.max(0, Math.min(1, bytesDownloaded / totalBytes)) : group.progress,
3447
+ resumable: group.resumable ?? (groupStatus === "paused" || groupStatus === "downloading" || groupStatus === "queued" || groupStatus === "error"),
3448
+ error: group.error,
3449
+ installedAt: groupStatus === "downloaded" ? state.installedAt ?? now : void 0,
3450
+ updatedAt: state.updatedAt ?? now,
3451
+ files: groupFiles
3452
+ });
3453
+ }
3454
+ if (selectedGroupId && !groupsState[selectedGroupId] && state.files.length > 0 && state.status !== "not-downloaded") {
3455
+ const manifestGroup = manifest?.groups[selectedGroupId];
3456
+ groupsState[selectedGroupId] = LocalModelLifecycleGroupStateSchema.parse({
3457
+ groupId: selectedGroupId,
3458
+ baseGroupId: manifestGroup?.baseGroupId ?? selectedGroupId,
3459
+ status: normalizeLiveStatusForStoredState(state.status),
3460
+ rootDir: manifestGroup?.rootDir,
3461
+ bytesDownloaded: state.bytesDownloaded,
3462
+ totalBytes: state.totalBytes,
3463
+ progress: state.progress,
3464
+ resumable: state.resumable,
3465
+ error: state.error,
3466
+ installedAt: state.installedAt,
3467
+ updatedAt: state.updatedAt ?? now,
3468
+ files: state.files.map((file) => LocalModelLifecycleFileStateSchema.parse({
3469
+ ...file,
3470
+ required: true,
3471
+ status: file.sizeBytes !== void 0 && (file.downloadedBytes ?? 0) >= file.sizeBytes ? "downloaded" : state.status === "downloaded" ? "downloaded" : normalizeLiveStatusForStoredState(state.status)
3472
+ }))
3473
+ });
3474
+ }
3475
+ return LocalModelAssetStateSchema.parse({
3476
+ ...state,
3477
+ selectedGroupId,
3478
+ groupsState
3479
+ });
3480
+ }
3481
+ function mergeHistoricalGroupsIntoManifest(input) {
3482
+ const existing = input.manifest;
3483
+ const groups = existing ? { ...existing.groups } : {};
3484
+ const groupOrder = existing ? [...existing.groupOrder] : [];
3485
+ const fallbackGroups = input.fallbackPlan?.groups ?? [];
3486
+ for (const fallbackGroup of fallbackGroups) {
3487
+ if (groups[fallbackGroup.id]) continue;
3488
+ if (!isConcreteCommitHash(fallbackGroup.commitHash) || !isConcreteCommitHash(fallbackGroup.shortCommitHash)) continue;
3489
+ const state = input.groupsState[fallbackGroup.id];
3490
+ const commitHash = fallbackGroup.commitHash;
3491
+ const shortCommitHash = fallbackGroup.shortCommitHash;
3492
+ groups[fallbackGroup.id] = {
3493
+ id: fallbackGroup.id,
3494
+ baseGroupId: fallbackGroup.baseGroupId ?? fallbackGroup.id,
3495
+ label: fallbackGroup.label,
3496
+ displayLabel: `${fallbackGroup.label} · ${shortCommitHash}`,
3497
+ description: fallbackGroup.description,
3498
+ profile: fallbackGroup.profile,
3499
+ dtype: fallbackGroup.dtype,
3500
+ commitHash,
3501
+ shortCommitHash,
3502
+ rootDir: fallbackGroup.rootDir ?? state?.rootDir ?? getLocalModelProfileGroupRoot(input.cacheDir, input.modelId, fallbackGroup.id),
3503
+ estimatedTotalBytes: fallbackGroup.estimatedTotalBytes,
3504
+ selectable: fallbackGroup.selectable,
3505
+ files: fallbackGroup.files.map((file) => ({
3506
+ ...file,
3507
+ revision: file.revision ?? commitHash
3508
+ }))
3509
+ };
3510
+ groupOrder.push(fallbackGroup.id);
3511
+ }
3512
+ if (!existing && groupOrder.length === 0) return void 0;
3513
+ return LocalModelProfileManifestSchema.parse({
3514
+ modelId: input.modelId,
3515
+ source: "huggingface",
3516
+ endpoint: existing?.endpoint ?? "",
3517
+ revision: existing?.revision ?? "legacy",
3518
+ commitHash: existing?.commitHash ?? "legacy",
3519
+ shortCommitHash: existing?.shortCommitHash ?? "legacy",
3520
+ fetchedAt: existing?.fetchedAt ?? 0,
3521
+ updatedAt: existing?.updatedAt ?? 0,
3522
+ raw: existing?.raw,
3523
+ groups,
3524
+ groupOrder
3525
+ });
3526
+ }
3527
+ function filterConcreteProfileManifest(manifest) {
3528
+ if (!manifest || !isConcreteCommitHash(manifest.commitHash)) return void 0;
3529
+ const groups = Object.fromEntries(manifest.groupOrder.flatMap((groupId) => {
3530
+ const group = manifest.groups[groupId];
3531
+ if (!group || !isConcreteCommitHash(group.commitHash)) return [];
3532
+ return [[groupId, group]];
3533
+ }));
3534
+ const groupOrder = manifest.groupOrder.filter((groupId) => groups[groupId]);
3535
+ if (groupOrder.length === 0) return void 0;
3536
+ return LocalModelProfileManifestSchema.parse({
3537
+ ...manifest,
3538
+ groups,
3539
+ groupOrder
3540
+ });
3541
+ }
3542
+ function isConcreteCommitHash(value) {
3543
+ return Boolean(value && value !== "legacy");
3544
+ }
3545
+ function formatManifestGroupChipLabel(manifest, group) {
3546
+ if (group.commitHash === manifest.commitHash) return group.label;
3547
+ return `${group.label} · ${group.shortCommitHash}`;
3548
+ }
3549
+ function buildPlanFromManifest(input) {
3550
+ const manifest = input.manifest;
3551
+ if (!manifest) return null;
3552
+ const selectedGroupId = input.selectedGroupId && manifest.groups[input.selectedGroupId]?.selectable ? input.selectedGroupId : selectFirstManifestGroupId(manifest);
3553
+ const groups = manifest.groupOrder.flatMap((groupId) => {
3554
+ const manifestGroup = manifest.groups[groupId];
3555
+ if (!manifestGroup) return [];
3556
+ const groupState = input.groupsState[groupId];
3557
+ return [{
3558
+ id: manifestGroup.id,
3559
+ label: formatManifestGroupChipLabel(manifest, manifestGroup),
3560
+ description: manifestGroup.description,
3561
+ profile: manifestGroup.profile,
3562
+ dtype: manifestGroup.dtype,
3563
+ estimatedTotalBytes: manifestGroup.estimatedTotalBytes,
3564
+ baseGroupId: manifestGroup.baseGroupId,
3565
+ commitHash: manifestGroup.commitHash,
3566
+ shortCommitHash: manifestGroup.shortCommitHash,
3567
+ rootDir: manifestGroup.rootDir,
3568
+ status: groupState?.status ?? "not-downloaded",
3569
+ progress: groupState?.progress,
3570
+ bytesDownloaded: groupState?.bytesDownloaded,
3571
+ totalBytes: groupState?.totalBytes ?? manifestGroup.estimatedTotalBytes,
3572
+ resumable: groupState?.resumable,
3573
+ error: groupState?.error,
3574
+ selectable: manifestGroup.selectable,
3575
+ selected: manifestGroup.id === selectedGroupId,
3576
+ files: manifestGroup.files.map((file) => ({
3577
+ ...file,
3578
+ required: file.required
3579
+ }))
3580
+ }];
3581
+ });
3582
+ const selectedGroup = groups.find((group) => group.id === selectedGroupId) ?? groups[0];
3583
+ if (!selectedGroup) return null;
3584
+ return {
3585
+ modelId: input.modelId,
3586
+ estimatedTotalBytes: selectedGroup.estimatedTotalBytes,
3587
+ files: selectedGroup.files,
3588
+ selectedGroupId: selectedGroup.id,
3589
+ groups
3590
+ };
3591
+ }
3592
+ function reconcileGroupFiles(input) {
3084
3593
  const currentFileByPath = new Map(input.currentFiles.map((file) => [file.path, file]));
3085
- return input.planFiles.map((file) => {
3086
- const downloadedBytes = currentFileByPath.get(file.path)?.downloadedBytes;
3087
- return {
3594
+ return input.manifestGroup.files.map((file) => {
3595
+ const current = currentFileByPath.get(file.path);
3596
+ const downloadedBytes = current?.downloadedBytes === void 0 ? 0 : file.sizeBytes === void 0 ? current.downloadedBytes : Math.min(current.downloadedBytes, file.sizeBytes);
3597
+ const status = file.sizeBytes !== void 0 && downloadedBytes >= file.sizeBytes ? "downloaded" : current?.status ?? "not-downloaded";
3598
+ return LocalModelLifecycleFileStateSchema.parse({
3088
3599
  path: file.path,
3089
3600
  sizeBytes: file.sizeBytes,
3090
- downloadedBytes: downloadedBytes === void 0 ? 0 : file.sizeBytes === void 0 ? downloadedBytes : Math.min(downloadedBytes, file.sizeBytes)
3091
- };
3601
+ downloadedBytes,
3602
+ required: file.required,
3603
+ status,
3604
+ updatedAt: current?.updatedAt,
3605
+ error: current?.error
3606
+ });
3092
3607
  });
3093
3608
  }
3609
+ function reconcileGroupFilesFromSnapshot(input) {
3610
+ const currentFileByPath = new Map(input.currentFiles.map((file) => [file.path, file]));
3611
+ return input.manifestGroup.files.map((file) => {
3612
+ const current = currentFileByPath.get(file.path);
3613
+ const downloadedBytes = current?.downloadedBytes === void 0 ? input.currentStatus === "downloaded" ? file.sizeBytes : 0 : file.sizeBytes === void 0 ? current.downloadedBytes : Math.min(current.downloadedBytes, file.sizeBytes);
3614
+ const status = current?.status ?? (file.sizeBytes !== void 0 && downloadedBytes !== void 0 && downloadedBytes >= file.sizeBytes ? "downloaded" : input.currentStatus === "downloaded" ? "downloaded" : input.currentStatus === "paused" || input.currentStatus === "downloading" || input.currentStatus === "error" ? input.currentStatus : "not-downloaded");
3615
+ return LocalModelLifecycleFileStateSchema.parse({
3616
+ path: file.path,
3617
+ sizeBytes: file.sizeBytes,
3618
+ downloadedBytes,
3619
+ required: file.required,
3620
+ status,
3621
+ updatedAt: current?.updatedAt,
3622
+ error: current?.error
3623
+ });
3624
+ });
3625
+ }
3626
+ async function reconcileGroupFilesFromDisk(input) {
3627
+ const currentFileByPath = new Map(input.currentFiles.map((file) => [file.path, file]));
3628
+ return Promise.all(input.manifestGroup.files.map(async (file) => {
3629
+ const current = currentFileByPath.get(file.path);
3630
+ const diskBytes = await readPathSize(join(input.rootDir, file.path));
3631
+ const downloadedBytes = diskBytes === null ? current?.downloadedBytes ?? 0 : file.sizeBytes === void 0 ? diskBytes : Math.min(diskBytes, file.sizeBytes);
3632
+ const status = file.sizeBytes !== void 0 && downloadedBytes >= file.sizeBytes ? "downloaded" : downloadedBytes > 0 ? "paused" : "not-downloaded";
3633
+ return LocalModelLifecycleFileStateSchema.parse({
3634
+ path: file.path,
3635
+ sizeBytes: file.sizeBytes,
3636
+ downloadedBytes,
3637
+ required: file.required,
3638
+ status,
3639
+ updatedAt: current?.updatedAt,
3640
+ error: current?.error
3641
+ });
3642
+ }));
3643
+ }
3644
+ function isActiveDownloadStatus(status) {
3645
+ return status === "queued" || status === "downloading" || status === "deleting";
3646
+ }
3647
+ function normalizeLiveStatusForStoredState(status) {
3648
+ if (status === "queued" || status === "downloading") return "paused";
3649
+ return status;
3650
+ }
3094
3651
  function sumDownloadedBytes(files) {
3095
3652
  return files.reduce((total, file) => {
3096
3653
  const downloadedBytes = file.downloadedBytes ?? 0;
@@ -3118,12 +3675,11 @@ function throwIfAborted(signal) {
3118
3675
  if (signal.aborted) throw new Error("Local model download aborted.");
3119
3676
  }
3120
3677
  async function downloadHuggingFaceFileToCacheDirWithProgress(input) {
3121
- const revision = "main";
3122
3678
  let lastError;
3123
3679
  const info = await readHuggingFaceFileDownloadInfoWithRetry({
3124
3680
  repo: input.repo,
3125
3681
  path: input.path,
3126
- revision,
3682
+ revision: input.revision,
3127
3683
  hubUrl: input.hubUrl,
3128
3684
  retryPolicy: input.retryPolicy,
3129
3685
  fetch: input.fetch,
@@ -3137,25 +3693,25 @@ async function downloadHuggingFaceFileToCacheDirWithProgress(input) {
3137
3693
  cacheDir: input.cacheDir,
3138
3694
  modelId: input.repo.name,
3139
3695
  filePath: input.path,
3140
- revision,
3141
- etag: info.etag
3696
+ revision: input.revision,
3697
+ etag: input.etag ?? info.etag
3142
3698
  });
3143
- const existingPointerSize = await readPathSize(cachePaths.pointerPath);
3144
- if (existingPointerSize !== null && existingPointerSize >= totalBytes) {
3699
+ const existingTargetSize = await readPathSize(input.targetPath);
3700
+ if (existingTargetSize !== null && existingTargetSize >= totalBytes) {
3145
3701
  await input.onProgress(totalBytes);
3146
- return cachePaths.pointerPath;
3702
+ return input.targetPath;
3147
3703
  }
3148
3704
  for (let attempt = 0; attempt <= input.retryPolicy.limit; attempt += 1) try {
3149
3705
  throwIfAborted(input.signal);
3150
- let resumeBytes = await readPathSize(cachePaths.incompletePath);
3706
+ let resumeBytes = await readPathSize(`${input.targetPath}.incomplete`);
3151
3707
  if (resumeBytes !== null && resumeBytes > totalBytes) {
3152
- await rm(cachePaths.incompletePath, { force: true });
3708
+ await rm(`${input.targetPath}.incomplete`, { force: true });
3153
3709
  resumeBytes = 0;
3154
3710
  }
3155
3711
  if (resumeBytes !== null && resumeBytes > 0) await input.onProgress(Math.min(resumeBytes, totalBytes));
3156
3712
  if (!await streamDownloadToIncompleteFile({
3157
3713
  url: info.url,
3158
- incompletePath: cachePaths.incompletePath,
3714
+ incompletePath: `${input.targetPath}.incomplete`,
3159
3715
  startBytes: resumeBytes ?? 0,
3160
3716
  totalBytes,
3161
3717
  accessToken: void 0,
@@ -3166,7 +3722,7 @@ async function downloadHuggingFaceFileToCacheDirWithProgress(input) {
3166
3722
  const blob = await downloadFile({
3167
3723
  repo: input.repo,
3168
3724
  path: input.path,
3169
- revision,
3725
+ revision: input.revision,
3170
3726
  hubUrl: input.hubUrl,
3171
3727
  fetch: input.fetch,
3172
3728
  downloadInfo: info,
@@ -3175,15 +3731,24 @@ async function downloadHuggingFaceFileToCacheDirWithProgress(input) {
3175
3731
  if (!blob) throw new Error(`Invalid response for file ${input.path}.`);
3176
3732
  await appendBlobToIncompleteFile({
3177
3733
  blob: resumeBytes && resumeBytes > 0 ? blob.slice(resumeBytes, totalBytes) : blob,
3178
- incompletePath: cachePaths.incompletePath,
3734
+ incompletePath: `${input.targetPath}.incomplete`,
3179
3735
  startBytes: resumeBytes ?? 0,
3180
3736
  totalBytes,
3181
3737
  onProgress: input.onProgress
3182
3738
  });
3183
3739
  }
3184
- await finalizeHubCacheFile(cachePaths);
3740
+ const incompleteSize = await readPathSize(`${input.targetPath}.incomplete`);
3741
+ if (incompleteSize === null || incompleteSize < totalBytes) throw new Error(`Incomplete response for file ${input.path}: downloaded ${incompleteSize ?? 0} of ${totalBytes} bytes.`);
3742
+ await finalizeDownloadedFile({
3743
+ incompletePath: `${input.targetPath}.incomplete`,
3744
+ targetPath: input.targetPath
3745
+ });
3746
+ await mirrorDownloadedFileToHubCache({
3747
+ targetPath: input.targetPath,
3748
+ cachePaths
3749
+ });
3185
3750
  await input.onProgress(totalBytes);
3186
- return cachePaths.pointerPath;
3751
+ return input.targetPath;
3187
3752
  } catch (error) {
3188
3753
  lastError = error;
3189
3754
  if (!isRetryableDownloadError(error) || attempt === input.retryPolicy.limit) throw error;
@@ -3266,13 +3831,18 @@ async function streamDownloadToIncompleteFile(input) {
3266
3831
  }
3267
3832
  return true;
3268
3833
  }
3269
- async function finalizeHubCacheFile(input) {
3270
- await mkdir(dirname(input.blobPath), { recursive: true });
3271
- await mkdir(dirname(input.pointerPath), { recursive: true });
3272
- await rm(input.blobPath, { force: true });
3273
- await rename(input.incompletePath, input.blobPath);
3274
- await unlink(input.pointerPath).catch(() => void 0);
3275
- await symlink(input.blobPath, input.pointerPath);
3834
+ async function finalizeDownloadedFile(input) {
3835
+ await mkdir(dirname(input.targetPath), { recursive: true });
3836
+ await rm(input.targetPath, { force: true });
3837
+ await rename(input.incompletePath, input.targetPath);
3838
+ }
3839
+ async function mirrorDownloadedFileToHubCache(input) {
3840
+ await mkdir(dirname(input.cachePaths.blobPath), { recursive: true });
3841
+ await mkdir(dirname(input.cachePaths.pointerPath), { recursive: true });
3842
+ await rm(input.cachePaths.blobPath, { force: true });
3843
+ await copyFile(input.targetPath, input.cachePaths.blobPath);
3844
+ await unlink(input.cachePaths.pointerPath).catch(() => void 0);
3845
+ await symlink(input.cachePaths.blobPath, input.cachePaths.pointerPath);
3276
3846
  }
3277
3847
  function getHubCacheFilePaths(input) {
3278
3848
  const repoPath = getHubCacheRepoPath(input.cacheDir, input.modelId);
@@ -3322,6 +3892,7 @@ function formatDuration(ms) {
3322
3892
  }
3323
3893
  async function mirrorHubCacheFileForTransformers(input) {
3324
3894
  const sourcePath = await resolveRealCacheFile(input.cachedPath);
3895
+ await copyFileIfMissing(sourcePath, join(input.profileRoot, input.filePath));
3325
3896
  await copyFileIfMissing(sourcePath, join(getTransformersLocalModelPath(input.cacheDir, input.modelId), input.filePath));
3326
3897
  await copyFileIfMissing(sourcePath, join(getTransformersFileCacheModelPath(input.cacheDir, input.modelId), input.filePath));
3327
3898
  }
@@ -3341,9 +3912,11 @@ function getHubCacheRepoPath(cacheDir, modelId) {
3341
3912
  return join(cacheDir, `models--${modelId.split("/").join("--")}`);
3342
3913
  }
3343
3914
  function toCatalogItem(candidate, asset) {
3344
- const hasSelectableGroup = candidate.downloadGroups?.some((group) => group.selectable) ?? false;
3915
+ const downloadGroups = asset.plan?.groups ?? candidate.downloadGroups;
3916
+ const hasSelectableGroup = downloadGroups?.some((group) => group.selectable) ?? false;
3345
3917
  return {
3346
3918
  ...candidate,
3919
+ downloadGroups,
3347
3920
  asset,
3348
3921
  selectable: hasSelectableGroup || (candidate.size.estimatedTotalBytes ?? 0) > 0,
3349
3922
  local: asset.status === "downloaded" || asset.status === "paused" || asset.status === "downloading" || (asset.progress ?? 0) > 0
@@ -5022,7 +5595,7 @@ const globalSettingsRouter = router({
5022
5595
  translationCache: TranslationCacheSettingsSchema.partial().optional(),
5023
5596
  translationEngines: z.object({
5024
5597
  openai: TranslationOpenAISettingsSchema.partial().optional(),
5025
- local: TranslationLocalSettingsSchema.partial().optional()
5598
+ local: TranslationLocalSettingsSchema.partial().extend({ selectedGroupId: z.string().min(1).nullable().optional() }).optional()
5026
5599
  }).optional()
5027
5600
  })).mutation(async ({ ctx, input }) => {
5028
5601
  await ctx.globalSettingsManager.writeSettings(input);
@@ -5113,6 +5686,18 @@ const localModelsRouter = router({
5113
5686
  })).query(({ ctx, input }) => {
5114
5687
  return ctx.localModelAssetService.readSelectedModelState(input.modelId, input.selectedGroupId);
5115
5688
  }),
5689
+ panelState: publicProcedure.input(z.object({
5690
+ modelId: z.string().min(1),
5691
+ selectedGroupId: z.string().min(1).optional()
5692
+ })).query(async ({ ctx, input }) => {
5693
+ const asset = await ctx.localModelAssetService.readSelectedModelState(input.modelId, input.selectedGroupId);
5694
+ return {
5695
+ modelId: input.modelId,
5696
+ selectedGroupId: input.selectedGroupId ?? asset.plan?.selectedGroupId,
5697
+ asset,
5698
+ downloadPlan: asset.plan ?? null
5699
+ };
5700
+ }),
5116
5701
  subscribeLogs: publicProcedure.subscription(({ ctx }) => {
5117
5702
  return ctx.localModelAssetService.subscribeLogs();
5118
5703
  }),
@@ -5122,21 +5707,40 @@ const localModelsRouter = router({
5122
5707
  }),
5123
5708
  download: publicProcedure.input(z.object({
5124
5709
  modelId: z.string().min(1),
5710
+ groupId: z.string().min(1).optional(),
5125
5711
  selectedGroupId: z.string().min(1).optional()
5126
5712
  })).mutation(async ({ ctx, input }) => {
5127
- return ctx.localModelAssetService.startDownload(input.modelId, input.selectedGroupId);
5713
+ return ctx.localModelAssetService.startDownload(input.modelId, input.groupId ?? input.selectedGroupId);
5128
5714
  }),
5129
- pause: publicProcedure.input(z.object({ modelId: z.string().min(1) })).mutation(({ ctx, input }) => {
5130
- return ctx.localModelAssetService.pauseDownload(input.modelId);
5715
+ pause: publicProcedure.input(z.object({
5716
+ modelId: z.string().min(1),
5717
+ groupId: z.string().min(1).optional(),
5718
+ selectedGroupId: z.string().min(1).optional()
5719
+ })).mutation(({ ctx, input }) => {
5720
+ return ctx.localModelAssetService.pauseDownload(input.modelId, input.groupId ?? input.selectedGroupId);
5131
5721
  }),
5132
5722
  resume: publicProcedure.input(z.object({
5133
5723
  modelId: z.string().min(1),
5724
+ groupId: z.string().min(1).optional(),
5134
5725
  selectedGroupId: z.string().min(1).optional()
5135
5726
  })).mutation(async ({ ctx, input }) => {
5136
- return ctx.localModelAssetService.resumeDownload(input.modelId, input.selectedGroupId);
5727
+ return ctx.localModelAssetService.resumeDownload(input.modelId, input.groupId ?? input.selectedGroupId);
5137
5728
  }),
5138
- delete: publicProcedure.input(z.object({ modelId: z.string().min(1) })).mutation(({ ctx, input }) => {
5139
- return ctx.localModelAssetService.deleteModel(input.modelId);
5729
+ delete: publicProcedure.input(z.object({
5730
+ modelId: z.string().min(1),
5731
+ groupId: z.string().min(1).optional(),
5732
+ selectedGroupId: z.string().min(1).optional()
5733
+ })).mutation(({ ctx, input }) => {
5734
+ return ctx.localModelAssetService.deleteModel(input.modelId, input.groupId ?? input.selectedGroupId);
5735
+ }),
5736
+ refreshProfiles: publicProcedure.input(z.object({ modelId: z.string().min(1).optional() })).mutation(async ({ ctx, input }) => {
5737
+ const asset = await ctx.localModelAssetService.refreshProfiles(input.modelId);
5738
+ return {
5739
+ modelId: asset.modelId,
5740
+ selectedGroupId: asset.selectedGroupId ?? asset.plan?.selectedGroupId,
5741
+ asset,
5742
+ downloadPlan: asset.plan ?? null
5743
+ };
5140
5744
  })
5141
5745
  });
5142
5746
  const OPSX_CORE_PROFILE_WORKFLOWS = [
@@ -5632,7 +6236,7 @@ const configRouter = router({
5632
6236
  translation: DocumentTranslationConfigSchema.partial().extend({ engines: z.object({
5633
6237
  local: z.object({
5634
6238
  model: z.string().min(1).optional(),
5635
- selectedGroupId: z.string().min(1).optional()
6239
+ selectedGroupId: z.string().min(1).nullable().optional()
5636
6240
  }).optional(),
5637
6241
  openai: z.object({ model: z.string().min(1).optional() }).optional()
5638
6242
  }).optional() }).optional()
@@ -6667,22 +7271,19 @@ var TranslationCacheService = class {
6667
7271
  //#endregion
6668
7272
  //#region src/translation-engine-service.ts
6669
7273
  var TranslationEngineService = class {
6670
- projectDir;
6671
7274
  configManager;
6672
7275
  globalSettingsManager;
6673
7276
  now;
6674
7277
  localCacheDir;
6675
7278
  localAssetStore;
6676
- localFetchCacheStore;
6677
7279
  constructor(options) {
6678
7280
  ensureProxyAwareFetchDispatcher();
6679
- this.projectDir = options.projectDir;
6680
7281
  this.configManager = options.configManager;
6681
7282
  this.globalSettingsManager = options.globalSettingsManager;
6682
7283
  this.now = options.now ?? Date.now;
6683
7284
  this.localCacheDir = options.localCacheDir ?? getDefaultLocalModelCacheDir();
6684
7285
  this.localAssetStore = new LocalModelAssetStore({ indexPath: options.localAssetIndexPath ?? getDefaultLocalModelIndexPath() });
6685
- this.localFetchCacheStore = new LocalModelFetchCacheStore({
7286
+ new LocalModelFetchCacheStore({
6686
7287
  cachePath: options.localFetchCachePath ?? getDefaultLocalModelFetchCachePath(),
6687
7288
  now: this.now
6688
7289
  });
@@ -6702,20 +7303,7 @@ var TranslationEngineService = class {
6702
7303
  }
6703
7304
  async getModelDownloadPlan(input) {
6704
7305
  if (input.engineId !== "local") return null;
6705
- const state = (await this.localAssetStore.readMap()).get(input.model);
6706
- const plan = await resolveLocalModelRuntimePlanFromProject({
6707
- projectDir: this.projectDir,
6708
- globalSettingsManager: this.globalSettingsManager,
6709
- modelId: input.model,
6710
- selectedGroupId: input.selectedGroupId,
6711
- cacheDir: this.localCacheDir,
6712
- fetchCacheStore: this.localFetchCacheStore,
6713
- loadTransformersModule: this.loadLocalTransformersModuleForPlan.bind(this)
6714
- }).catch(() => null);
6715
- const fallbackPlan = selectPersistedLocalPlan(state, input.selectedGroupId);
6716
- const effectivePlan = plan ?? fallbackPlan;
6717
- if (!effectivePlan) return null;
6718
- return enrichDownloadPlanWithAssetSnapshot(effectivePlan, state, input.selectedGroupId);
7306
+ return selectPersistedLocalPlan((await this.localAssetStore.readMap()).get(input.model), input.selectedGroupId);
6719
7307
  }
6720
7308
  async selectEngine(engineId) {
6721
7309
  await this.configManager.writeConfig({ translation: { engineId } });
@@ -6731,14 +7319,26 @@ var TranslationEngineService = class {
6731
7319
  (async () => {
6732
7320
  try {
6733
7321
  if (input.engineId === "browser") throw new Error("Browser translator runs in the browser runtime.");
6734
- const dtype = await this.readLocalDtype(input.engineId, input.model, input.selectedGroupId);
6735
- if (input.engineId === "local" && input.model) await this.assertLocalModelReady(input.model, input.selectedGroupId);
6736
- const translator = await (await this.loadFactory(input.engineId, input.model)).create({
7322
+ const settingsSnapshot = await this.globalSettingsManager.readSettings();
7323
+ const effectiveModel = resolveBatchTranslateModel(input, settingsSnapshot);
7324
+ if (input.engineId === "local") {
7325
+ const directionCheck = checkLocalDirectionalModelLanguagePair({
7326
+ model: effectiveModel,
7327
+ sourceLanguage: input.sourceLanguage,
7328
+ targetLanguage: input.targetLanguage
7329
+ });
7330
+ if (!directionCheck.supported) throw new Error(directionCheck.message ?? "Selected local model does not support the requested translation direction.");
7331
+ }
7332
+ const effectiveSelectedGroupId = input.engineId === "local" ? input.selectedGroupId ?? settingsSnapshot.translationEngines.local.selectedGroupId : void 0;
7333
+ const dtype = await this.readLocalDtype(input.engineId, effectiveModel, effectiveSelectedGroupId);
7334
+ if (input.engineId === "local" && effectiveModel) await this.assertLocalModelReady(effectiveModel, effectiveSelectedGroupId);
7335
+ const runtimeConfig = input.engineId === "local" && effectiveModel ? await this.readLocalRuntimeConfig(effectiveModel, effectiveSelectedGroupId) : void 0;
7336
+ const translator = await (await this.loadFactory(input.engineId, effectiveModel, settingsSnapshot)).create({
6737
7337
  sourceLanguage: input.sourceLanguage,
6738
7338
  targetLanguage: input.targetLanguage,
6739
- model: input.model,
7339
+ model: effectiveModel,
6740
7340
  dtype,
6741
- runtimeConfig: input.engineId === "local" && input.model ? await this.readLocalRuntimeConfig(input.model) : void 0,
7341
+ runtimeConfig,
6742
7342
  signal: controller.signal
6743
7343
  });
6744
7344
  try {
@@ -6764,11 +7364,11 @@ var TranslationEngineService = class {
6764
7364
  if (engineId !== "local" || !model) return void 0;
6765
7365
  const effectiveSelectedGroupId = selectedGroupId ?? (await this.globalSettingsManager.readSettings()).translationEngines.local.selectedGroupId;
6766
7366
  if (!effectiveSelectedGroupId) return void 0;
6767
- return (await this.getModelDownloadPlan({
7367
+ return selectLocalPlanGroup(await this.getModelDownloadPlan({
6768
7368
  engineId: "local",
6769
7369
  model,
6770
7370
  selectedGroupId: effectiveSelectedGroupId
6771
- }))?.groups?.find((group) => group.id === effectiveSelectedGroupId)?.dtype;
7371
+ }), effectiveSelectedGroupId)?.dtype;
6772
7372
  }
6773
7373
  async assertLocalModelReady(model, selectedGroupId) {
6774
7374
  const plan = await this.getModelDownloadPlan({
@@ -6776,89 +7376,57 @@ var TranslationEngineService = class {
6776
7376
  model,
6777
7377
  selectedGroupId
6778
7378
  });
6779
- const files = (plan?.groups?.find((group) => group.id === (selectedGroupId ?? plan.selectedGroupId)) ?? plan?.groups?.find((group) => group.selected))?.files ?? plan?.files ?? [];
6780
- if (!plan || files.length === 0) throw new Error("No local runtime file plan is available for the selected model.");
6781
- const cacheStatus = await readLocalModelFileStatus({
6782
- cacheDir: this.localCacheDir,
6783
- modelId: model,
6784
- files: files.map((file) => file.path)
6785
- });
6786
- if (cacheStatus.allCached) {
6787
- const current = (await this.localAssetStore.readMap()).get(model);
6788
- if (current) await this.localAssetStore.upsert({
6789
- ...current,
6790
- status: "downloaded",
6791
- progress: 1,
6792
- bytesDownloaded: plan.estimatedTotalBytes ?? current.bytesDownloaded,
6793
- totalBytes: plan.estimatedTotalBytes ?? current.totalBytes,
6794
- resumable: false,
6795
- error: void 0,
6796
- plan,
6797
- files: files.map((file) => ({
6798
- path: file.path,
6799
- sizeBytes: file.sizeBytes,
6800
- downloadedBytes: file.sizeBytes
6801
- })),
6802
- installedAt: current.installedAt ?? this.now(),
6803
- updatedAt: this.now()
6804
- });
6805
- return;
7379
+ const selectedGroup = selectLocalPlanGroup(plan, selectedGroupId);
7380
+ if (!plan || !selectedGroup || selectedGroup.files.length === 0) throw new Error("No local runtime file plan is available for the selected model.");
7381
+ const files = selectedGroup.files;
7382
+ const selectedGroupState = await this.readSelectedLocalGroupState(model, selectedGroup.id);
7383
+ if (selectedGroupState?.status === "downloaded" && selectedGroup.rootDir) {
7384
+ if ((await readMissingLocalGroupFiles(selectedGroup.rootDir, files)).length === 0) return;
6806
7385
  }
6807
- const allMissingFiles = cacheStatus.files.filter((file) => !file.cached).map((file) => file.file);
7386
+ if (selectedGroupState?.status === "downloaded") return;
7387
+ const allMissingFiles = selectedGroup.rootDir ? await readMissingLocalGroupFiles(selectedGroup.rootDir, files) : files.map((file) => file.path);
6808
7388
  const missingFiles = allMissingFiles.slice(0, 3);
6809
7389
  const suffix = allMissingFiles.length > missingFiles.length ? ` and ${allMissingFiles.length - missingFiles.length} more` : "";
6810
7390
  throw new Error(`Selected local model files are not installed locally: ${missingFiles.join(", ")}${suffix}.`);
6811
7391
  }
6812
- async readLocalRuntimeConfig(model) {
7392
+ async readLocalRuntimeConfig(model, selectedGroupId) {
7393
+ const selectedGroup = selectLocalPlanGroup(await this.getModelDownloadPlan({
7394
+ engineId: "local",
7395
+ model,
7396
+ selectedGroupId
7397
+ }), selectedGroupId);
7398
+ const configPath = selectedGroup?.rootDir ? join(selectedGroup.rootDir, "config.json") : join(this.localCacheDir, "models", model, "config.json");
6813
7399
  try {
6814
- return JSON.parse(await readFile(join(this.localCacheDir, "models", model, "config.json"), "utf8"));
7400
+ return JSON.parse(await readFile(configPath, "utf8"));
6815
7401
  } catch {
6816
7402
  return;
6817
7403
  }
6818
7404
  }
6819
- async loadFactory(engineId, model) {
6820
- const globalSettings = await this.globalSettingsManager.readSettings();
6821
- if (engineId === "local") return (await import("./src-DBFY1eQK.mjs")).createLocalTranslatorFactory({
7405
+ async readSelectedLocalGroupState(model, selectedGroupId) {
7406
+ return (await this.localAssetStore.readMap()).get(model)?.groupsState[selectedGroupId];
7407
+ }
7408
+ async loadFactory(engineId, model, settingsSnapshot) {
7409
+ const globalSettings = settingsSnapshot ?? await this.globalSettingsManager.readSettings();
7410
+ if (engineId === "local") return (await import("./src-DPZ2-0Yn.mjs")).createLocalTranslatorFactory({
6822
7411
  defaultModel: model ?? globalSettings.translationEngines.local.model,
6823
7412
  cacheDir: this.localCacheDir,
6824
7413
  localOnly: true
6825
7414
  });
6826
- return (await import("./src-DuQ_-3Kn.mjs")).createOpenAICompletionTranslatorFactory({
7415
+ return (await import("./src-CoUhFB25.mjs")).createOpenAICompletionTranslatorFactory({
6827
7416
  baseUrl: globalSettings.translationEngines.openai.baseUrl,
6828
7417
  token: globalSettings.translationEngines.openai.token,
6829
7418
  model: model ?? globalSettings.translationEngines.openai.model
6830
7419
  });
6831
7420
  }
6832
- async loadLocalTransformersModuleForPlan(_projectDir, _globalSettingsManager) {
6833
- return await import("@huggingface/transformers");
6834
- }
6835
7421
  };
6836
- function enrichDownloadPlanWithAssetSnapshot(plan, state, selectedGroupId) {
6837
- if (!state?.plan) return plan;
6838
- const assetGroup = state.plan.groups?.find((group) => group.id === (selectedGroupId ?? plan.selectedGroupId));
6839
- const mergedGroups = plan.groups?.map((group) => {
6840
- const matchingAssetGroup = state.plan?.groups?.find((asset) => asset.id === group.id);
6841
- if (!matchingAssetGroup) return group;
6842
- return {
6843
- ...group,
6844
- estimatedTotalBytes: group.estimatedTotalBytes ?? matchingAssetGroup.estimatedTotalBytes,
6845
- files: group.files.map((file) => {
6846
- const matchingAssetFile = matchingAssetGroup.files.find((asset) => asset.path === file.path);
6847
- return matchingAssetFile?.sizeBytes !== void 0 && file.sizeBytes === void 0 ? {
6848
- ...file,
6849
- sizeBytes: matchingAssetFile.sizeBytes
6850
- } : file;
6851
- })
6852
- };
6853
- });
6854
- return {
6855
- ...plan,
6856
- estimatedTotalBytes: plan.estimatedTotalBytes ?? assetGroup?.estimatedTotalBytes ?? state.plan.estimatedTotalBytes,
6857
- groups: mergedGroups
6858
- };
7422
+ function resolveBatchTranslateModel(input, settings) {
7423
+ if (input.model) return input.model;
7424
+ if (input.engineId === "local") return settings.translationEngines.local.model;
7425
+ if (input.engineId === "openai") return settings.translationEngines.openai.model;
6859
7426
  }
6860
7427
  function selectPersistedLocalPlan(state, selectedGroupId) {
6861
- const plan = state?.plan;
7428
+ if (!state) return null;
7429
+ const plan = LocalModelAssetStateSchema.parse(state).plan;
6862
7430
  if (!plan) return null;
6863
7431
  if (!selectedGroupId || !plan.groups?.length) return {
6864
7432
  ...plan,
@@ -6868,7 +7436,7 @@ function selectPersistedLocalPlan(state, selectedGroupId) {
6868
7436
  files: [...group.files]
6869
7437
  }))
6870
7438
  };
6871
- const selectedGroup = plan.groups.find((group) => group.id === selectedGroupId);
7439
+ const selectedGroup = selectLocalPlanGroup(plan, selectedGroupId);
6872
7440
  if (!selectedGroup) return null;
6873
7441
  return {
6874
7442
  modelId: plan.modelId,
@@ -6882,6 +7450,22 @@ function selectPersistedLocalPlan(state, selectedGroupId) {
6882
7450
  }))
6883
7451
  };
6884
7452
  }
7453
+ function selectLocalPlanGroup(plan, selectedGroupId) {
7454
+ if (!plan?.groups?.length) return void 0;
7455
+ const requestedGroupId = selectedGroupId ?? plan.selectedGroupId;
7456
+ return plan.groups.find((group) => group.id === requestedGroupId) ?? plan.groups.find((group) => group.baseGroupId === requestedGroupId) ?? plan.groups.find((group) => group.selected) ?? plan.groups[0];
7457
+ }
7458
+ async function readMissingLocalGroupFiles(rootDir, files) {
7459
+ return (await Promise.all(files.map(async (file) => {
7460
+ try {
7461
+ const entry = await stat(join(rootDir, file.path));
7462
+ if (file.sizeBytes !== void 0 && entry.size < file.sizeBytes) return file.path;
7463
+ return null;
7464
+ } catch {
7465
+ return file.path;
7466
+ }
7467
+ }))).filter((file) => file !== null);
7468
+ }
6885
7469
 
6886
7470
  //#endregion
6887
7471
  //#region src/workflow-invocation-service.ts
@@ -7178,6 +7762,7 @@ function createServer(config) {
7178
7762
  });
7179
7763
  const nmtModelCacheDir = config.runtimePaths?.localModelCacheDir ?? getDefaultLocalModelCacheDir();
7180
7764
  const nmtModelIndexPath = config.runtimePaths?.localModelAssetIndexPath ?? getDefaultLocalModelIndexPath();
7765
+ const nmtModelProfileManifestPath = config.runtimePaths?.localModelProfileManifestPath ?? getDefaultLocalModelProfileManifestPath();
7181
7766
  const nmtModelFetchCachePath = config.runtimePaths?.localModelFetchCachePath ?? getDefaultLocalModelFetchCachePath();
7182
7767
  const translationEngineService = new TranslationEngineService({
7183
7768
  projectDir: config.projectDir,
@@ -7193,6 +7778,7 @@ function createServer(config) {
7193
7778
  globalSettingsManager,
7194
7779
  cacheDir: nmtModelCacheDir,
7195
7780
  indexPath: nmtModelIndexPath,
7781
+ profileManifestPath: nmtModelProfileManifestPath,
7196
7782
  fetchCachePath: nmtModelFetchCachePath
7197
7783
  });
7198
7784
  const watcher = config.enableWatcher !== false ? new OpenSpecWatcher(config.projectDir) : void 0;