ethagent 3.3.3 → 4.0.0
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/.claude-plugin/marketplace.json +11 -0
- package/.claude-plugin/plugin.json +35 -0
- package/LICENSE +1 -1
- package/README.md +64 -104
- package/commands/ethagent.md +40 -0
- package/package.json +16 -16
- package/src/app/keybindings/KeybindingProvider.tsx +1 -6
- package/src/app/keybindings/types.ts +1 -6
- package/src/cli/ResetConfirmView.tsx +54 -53
- package/src/cli/demo.ts +86 -0
- package/src/cli/hookIo.ts +45 -0
- package/src/cli/main.tsx +94 -123
- package/src/cli/memoryGuard.ts +49 -0
- package/src/cli/reset.ts +28 -70
- package/src/cli/sessionStart.ts +33 -0
- package/src/cli/status.ts +46 -0
- package/src/cli/sync.ts +167 -0
- package/src/cli/syncAdapters/claude-code.ts +86 -0
- package/src/cli/syncAdapters/codex.ts +66 -0
- package/src/cli/syncAdapters/index.ts +45 -0
- package/src/cli/syncAdapters/managedBlock.ts +175 -0
- package/src/cli/syncAdapters/shared.ts +63 -0
- package/src/identity/continuity/envelopeParse.ts +20 -1
- package/src/identity/continuity/publicSkills.ts +3 -1
- package/src/identity/continuity/skills/publicSkillsSync.ts +2 -1
- package/src/identity/continuity/skills/scaffold.ts +5 -2
- package/src/identity/continuity/snapshots.ts +12 -5
- package/src/identity/continuity/storage/defaults.ts +20 -19
- package/src/identity/continuity/storage/status.ts +1 -1
- package/src/identity/ens/ensLookup/constants.ts +1 -1
- package/src/identity/manager/IdentityManager.tsx +33 -0
- package/src/identity/{hub → manager}/OperationalRoutes.tsx +37 -18
- package/src/identity/{hub → manager}/Routes.tsx +48 -34
- package/src/identity/{hub → manager}/continuity/ContinuityDashboardScreen.tsx +9 -19
- package/src/identity/{hub → manager}/continuity/RebackupStorageScreen.tsx +3 -3
- package/src/identity/manager/continuity/RecoveryConfirmScreen.tsx +102 -0
- package/src/identity/{hub → manager}/continuity/SavePromptScreen.tsx +2 -3
- package/src/identity/{hub → manager}/continuity/completion.ts +1 -1
- package/src/identity/{hub → manager}/continuity/effects.ts +1 -1
- package/src/identity/{hub → manager}/continuity/skills/DeleteSkillConfirmScreen.tsx +2 -2
- package/src/identity/{hub → manager}/continuity/skills/NewSkillScreen.tsx +0 -5
- package/src/identity/{hub → manager}/continuity/skills/NewSkillVisibilityScreen.tsx +4 -4
- package/src/identity/{hub → manager}/continuity/skills/SkillActionsScreen.tsx +6 -22
- package/src/identity/{hub → manager}/continuity/skills/SkillsTreeScreen.tsx +5 -17
- package/src/identity/{hub → manager}/continuity/snapshot.ts +1 -1
- package/src/identity/{hub → manager}/continuity/vault.ts +1 -1
- package/src/identity/{hub → manager}/create/CreateFlow.tsx +59 -32
- package/src/identity/{hub → manager}/create/effects.ts +19 -10
- package/src/identity/manager/create/importScan.ts +122 -0
- package/src/identity/{hub → manager}/custody/CustodyEditFlow.tsx +17 -61
- package/src/identity/{hub → manager}/custody/actions.ts +1 -15
- package/src/identity/{hub → manager}/custody/routes.tsx +20 -40
- package/src/identity/{hub → manager}/custody/transactions.ts +1 -0
- package/src/identity/{hub → manager}/custody/types.ts +1 -2
- package/src/identity/{hub → manager}/custody/useCustodyEffects.ts +1 -1
- package/src/identity/{hub → manager}/ens/EnsEditAdvancedScreens.tsx +2 -2
- package/src/identity/{hub → manager}/ens/EnsEditMaintenanceScreens.tsx +12 -23
- package/src/identity/{hub → manager}/ens/EnsEditReviewScreens.tsx +18 -42
- package/src/identity/{hub → manager}/ens/EnsEditRunners.tsx +1 -1
- package/src/identity/{hub → manager}/ens/EnsEditShared.tsx +0 -2
- package/src/identity/{hub → manager}/ens/EnsEditSimpleScreens.tsx +10 -19
- package/src/identity/{hub → manager}/ens/EnsFlow.tsx +133 -41
- package/src/identity/{hub → manager}/ens/EnsOperatorWalletsScreen.tsx +14 -19
- package/src/identity/{hub → manager}/ens/editCopy.ts +1 -14
- package/src/identity/{hub → manager}/profile/EditProfileFlow.tsx +99 -66
- package/src/identity/{hub → manager}/profile/effects.ts +1 -3
- package/src/identity/{hub → manager}/profile/operatorSave.ts +1 -1
- package/src/identity/{hub → manager}/profile/state.ts +1 -1
- package/src/identity/{hub/identityHubReducer.ts → manager/reducer.ts} +25 -26
- package/src/identity/{hub → manager}/restore/RestoreFlow.tsx +16 -24
- package/src/identity/{hub → manager}/restore/apply.ts +1 -1
- package/src/identity/{hub → manager}/restore/auth.ts +1 -1
- package/src/identity/{hub → manager}/restore/discover.ts +1 -1
- package/src/identity/{hub → manager}/restore/fetch.ts +1 -1
- package/src/identity/{hub → manager}/restore/restoreAdmin.ts +1 -1
- package/src/identity/{hub → manager}/restore/useRestoreEffects.ts +2 -9
- package/src/identity/{hub → manager}/settings/StorageCredentialScreen.tsx +10 -25
- package/src/identity/{hub → manager}/shared/components/DetailsScreen.tsx +5 -7
- package/src/identity/{hub → manager}/shared/components/ErrorScreen.tsx +6 -10
- package/src/identity/{hub → manager}/shared/components/FlowTimeline.tsx +4 -3
- package/src/identity/{hub → manager}/shared/components/IdentitySummary.tsx +19 -59
- package/src/identity/manager/shared/components/LazyMenu.tsx +147 -0
- package/src/identity/manager/shared/components/MenuScreen.tsx +220 -0
- package/src/identity/manager/shared/components/OperationCompleteScreen.tsx +28 -0
- package/src/identity/{hub → manager}/shared/components/UnlinkedIdentityScreen.tsx +9 -10
- package/src/identity/{hub → manager}/shared/components/WalletApprovalScreen.tsx +1 -2
- package/src/identity/manager/shared/components/Wordmark.tsx +54 -0
- package/src/identity/{hub → manager}/shared/components/menuFlagsFromReconciliation.ts +39 -15
- package/src/identity/{hub → manager}/shared/effects/profilePrep.ts +1 -1
- package/src/identity/manager/shared/effects/types.ts +30 -0
- package/src/identity/{hub → manager}/shared/model/copy.ts +0 -4
- package/src/identity/{hub → manager}/shared/model/errors.ts +32 -3
- package/src/identity/{hub → manager}/shared/model/network.ts +2 -2
- package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/hook.ts +5 -0
- package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/run.ts +1 -1
- package/src/identity/{hub/shared/reconciliation/useAgentReconciliation.ts → manager/shared/reconciliation/index.ts} +6 -0
- package/src/identity/{hub → manager}/shared/utils.ts +6 -10
- package/src/identity/{hub → manager}/transfer/TokenTransferFlow.tsx +3 -3
- package/src/identity/{hub → manager}/transfer/TokenTransferScreens.tsx +4 -10
- package/src/identity/{hub → manager}/transfer/effects.ts +1 -1
- package/src/identity/{hub → manager}/types.ts +5 -6
- package/src/identity/{hub/useIdentityHubContinuity.ts → manager/useContinuity.ts} +59 -27
- package/src/identity/{hub/useIdentityHubController.ts → manager/useController.ts} +38 -35
- package/src/identity/{hub/useIdentityHubSideEffects.ts → manager/useSideEffects.ts} +40 -4
- package/src/identity/registry/erc8004/discovery.ts +3 -17
- package/src/identity/registry/erc8004/utils.ts +1 -1
- package/src/identity/storage/ipfs.ts +21 -1
- package/src/identity/wallet/browserWallet/html.ts +10 -2
- package/src/identity/wallet/browserWallet/http.ts +18 -0
- package/src/identity/wallet/browserWallet/requestServer.ts +5 -1
- package/src/identity/wallet/browserWallet/requests.ts +10 -28
- package/src/identity/wallet/browserWallet/session.ts +26 -33
- package/src/identity/wallet/browserWallet/validation.ts +14 -0
- package/src/identity/wallet/browserWallet/walletPageSource.ts +22 -40
- package/src/identity/wallet/page/boot.ts +43 -0
- package/src/identity/wallet/page/config.ts +59 -0
- package/src/identity/wallet/page/constants.ts +12 -0
- package/src/identity/wallet/page/copy.ts +47 -68
- package/src/identity/wallet/page/css.ts +638 -0
- package/src/identity/wallet/page/{errorView.ts → errors.ts} +5 -14
- package/src/identity/wallet/page/{controller.ts → flow.ts} +4 -71
- package/src/identity/wallet/page/markup.ts +44 -34
- package/src/identity/wallet/page/{walletProvider.ts → provider.ts} +0 -3
- package/src/identity/wallet/page/resize.ts +95 -0
- package/src/identity/wallet/page/state.ts +135 -8
- package/src/identity/wallet/page/timeline.ts +161 -0
- package/src/identity/wallet/page/view.ts +22 -302
- package/src/storage/config.ts +30 -80
- package/src/storage/reset.ts +31 -0
- package/src/storage/secrets.ts +1 -16
- package/src/ui/Select.tsx +27 -5
- package/src/ui/Spinner.tsx +16 -15
- package/src/ui/Surface.tsx +21 -17
- package/src/ui/TextArea.tsx +173 -0
- package/src/ui/TextInput.tsx +31 -133
- package/src/ui/theme.ts +22 -13
- package/src/utils/clipboard.ts +0 -140
- package/src/app/FirstRun.tsx +0 -577
- package/src/app/FirstRunTimeline.tsx +0 -51
- package/src/app/firstRunConfig.ts +0 -26
- package/src/app/hooks/useCancelRequest.ts +0 -22
- package/src/app/hooks/useDoublePress.ts +0 -46
- package/src/app/hooks/useExitOnCtrlC.ts +0 -36
- package/src/auth/openaiOAuth/credentials.ts +0 -47
- package/src/auth/openaiOAuth/crypto.ts +0 -23
- package/src/auth/openaiOAuth/index.ts +0 -238
- package/src/auth/openaiOAuth/landingPage.ts +0 -116
- package/src/auth/openaiOAuth/listener.ts +0 -151
- package/src/auth/openaiOAuth/refresh.ts +0 -70
- package/src/auth/openaiOAuth/shared.ts +0 -115
- package/src/chat/ChatBottomPane.tsx +0 -296
- package/src/chat/ChatScreen.tsx +0 -1685
- package/src/chat/ConversationStack.tsx +0 -56
- package/src/chat/MessageList.tsx +0 -638
- package/src/chat/SessionStatus.tsx +0 -53
- package/src/chat/chatEnvironment.ts +0 -16
- package/src/chat/chatScreenUtils.ts +0 -194
- package/src/chat/chatSessionState.ts +0 -146
- package/src/chat/chatTurnContext.ts +0 -50
- package/src/chat/chatTurnOrchestrator.ts +0 -603
- package/src/chat/chatTurnRows.ts +0 -64
- package/src/chat/commands.ts +0 -494
- package/src/chat/continuityEditReview.ts +0 -42
- package/src/chat/display/DiffView.tsx +0 -193
- package/src/chat/display/SyntaxText.tsx +0 -192
- package/src/chat/display/toolCallDisplay.ts +0 -103
- package/src/chat/display/toolResultDisplay.ts +0 -19
- package/src/chat/input/ChatInput.tsx +0 -625
- package/src/chat/input/chatInputHelpers.ts +0 -62
- package/src/chat/input/chatInputState.ts +0 -247
- package/src/chat/input/chatPaste.ts +0 -49
- package/src/chat/input/imageRefs.ts +0 -30
- package/src/chat/input/inputRendering.tsx +0 -93
- package/src/chat/input/textCursor.ts +0 -212
- package/src/chat/messageMarkdown.ts +0 -220
- package/src/chat/messageRows.ts +0 -43
- package/src/chat/planImplementation.ts +0 -62
- package/src/chat/slashCommandHandlers.ts +0 -122
- package/src/chat/slashCommandViews.ts +0 -120
- package/src/chat/transcript/TranscriptView.tsx +0 -184
- package/src/chat/transcript/transcriptViewport.ts +0 -295
- package/src/chat/views/ContextLimitView.tsx +0 -95
- package/src/chat/views/ContinuityEditReviewView.tsx +0 -50
- package/src/chat/views/CopyPicker.tsx +0 -50
- package/src/chat/views/PermissionPrompt.tsx +0 -156
- package/src/chat/views/PermissionsView.tsx +0 -165
- package/src/chat/views/PlanApprovalView.tsx +0 -91
- package/src/chat/views/ResumeView.tsx +0 -273
- package/src/chat/views/RewindView.tsx +0 -412
- package/src/cli/preview.tsx +0 -14
- package/src/cli/updateNotice.ts +0 -54
- package/src/identity/continuity/privateEdit/apply.ts +0 -170
- package/src/identity/continuity/privateEdit/diff.ts +0 -6
- package/src/identity/continuity/privateEdit/files.ts +0 -23
- package/src/identity/continuity/privateEdit/types.ts +0 -28
- package/src/identity/continuity/privateEdit.ts +0 -46
- package/src/identity/hub/IdentityHub.tsx +0 -14
- package/src/identity/hub/continuity/RecoveryConfirmScreen.tsx +0 -104
- package/src/identity/hub/ens/effects.ts +0 -218
- package/src/identity/hub/shared/components/MenuScreen.tsx +0 -241
- package/src/identity/hub/shared/effects/types.ts +0 -53
- package/src/identity/hub/shared/reconciliation/index.ts +0 -14
- package/src/identity/wallet/page/grainient.ts +0 -278
- package/src/identity/wallet/page/html.ts +0 -28
- package/src/identity/wallet/page/styles/base.ts +0 -259
- package/src/identity/wallet/page/styles/components.ts +0 -262
- package/src/identity/wallet/page/styles/index.ts +0 -5
- package/src/identity/wallet/page/styles/responsive.ts +0 -247
- package/src/identity/wallet/page.tsx +0 -38
- package/src/mcp/approvals.ts +0 -113
- package/src/mcp/config.ts +0 -235
- package/src/mcp/manager.ts +0 -482
- package/src/mcp/managerHelpers.ts +0 -70
- package/src/mcp/names.ts +0 -19
- package/src/mcp/output.ts +0 -96
- package/src/models/ModelPicker.tsx +0 -1009
- package/src/models/catalog.ts +0 -327
- package/src/models/huggingface.ts +0 -712
- package/src/models/huggingfaceStorage.ts +0 -136
- package/src/models/llamacpp.ts +0 -848
- package/src/models/llamacppCommands.ts +0 -44
- package/src/models/llamacppConfig.ts +0 -34
- package/src/models/llamacppDiscovery.ts +0 -176
- package/src/models/llamacppOutput.ts +0 -65
- package/src/models/llamacppPreflight.ts +0 -158
- package/src/models/modelDisplay.ts +0 -180
- package/src/models/modelPickerCatalogFlow.ts +0 -56
- package/src/models/modelPickerCredentials.ts +0 -166
- package/src/models/modelPickerData.ts +0 -41
- package/src/models/modelPickerDisplay.tsx +0 -132
- package/src/models/modelPickerHfFlow.ts +0 -192
- package/src/models/modelPickerLocalRunnerFlow.ts +0 -115
- package/src/models/modelPickerOptions.ts +0 -457
- package/src/models/modelPickerTypes.ts +0 -69
- package/src/models/modelPickerUninstallFlow.ts +0 -48
- package/src/models/modelPickerViewHelpers.ts +0 -174
- package/src/models/modelRecommendation.ts +0 -139
- package/src/models/providerDisplay.ts +0 -16
- package/src/models/runtimeDetection.ts +0 -81
- package/src/models/uncensoredCatalog.ts +0 -86
- package/src/providers/anthropic.ts +0 -290
- package/src/providers/contracts.ts +0 -71
- package/src/providers/errors.ts +0 -80
- package/src/providers/gemini.ts +0 -391
- package/src/providers/openai-chat.ts +0 -474
- package/src/providers/openai-responses-format.ts +0 -177
- package/src/providers/openai-responses.ts +0 -306
- package/src/providers/openaiChatWire.ts +0 -124
- package/src/providers/registry.ts +0 -120
- package/src/providers/retry.ts +0 -58
- package/src/providers/sse.ts +0 -93
- package/src/runtime/compaction.ts +0 -395
- package/src/runtime/cwd.ts +0 -43
- package/src/runtime/providerTurn.ts +0 -38
- package/src/runtime/sessionMode.ts +0 -55
- package/src/runtime/systemPrompt.ts +0 -213
- package/src/runtime/textToolParser.ts +0 -161
- package/src/runtime/toolClaimGuards.ts +0 -143
- package/src/runtime/toolExecution.ts +0 -304
- package/src/runtime/toolIntent.ts +0 -143
- package/src/runtime/turn.ts +0 -369
- package/src/runtime/turnNudges.ts +0 -223
- package/src/runtime/turnTypes.ts +0 -86
- package/src/storage/factoryReset.ts +0 -127
- package/src/storage/history.ts +0 -58
- package/src/storage/permissions.ts +0 -76
- package/src/storage/rewind.ts +0 -266
- package/src/storage/sessionExport.ts +0 -49
- package/src/storage/sessions.ts +0 -495
- package/src/tools/bashSafety.ts +0 -186
- package/src/tools/bashTool.ts +0 -140
- package/src/tools/changeDirectoryTool.ts +0 -213
- package/src/tools/contracts.ts +0 -192
- package/src/tools/deleteFileTool.ts +0 -116
- package/src/tools/editTool.ts +0 -165
- package/src/tools/editUtils.ts +0 -170
- package/src/tools/fileDiff.ts +0 -261
- package/src/tools/listDirectoryTool.ts +0 -55
- package/src/tools/listSkillFilesTool.ts +0 -77
- package/src/tools/listSkillsTool.ts +0 -68
- package/src/tools/mcpResourceTools.ts +0 -95
- package/src/tools/permissionRules.ts +0 -85
- package/src/tools/privateContinuityEditTool.ts +0 -187
- package/src/tools/privateContinuityReadTool.ts +0 -106
- package/src/tools/readSkillTool.ts +0 -107
- package/src/tools/readTool.ts +0 -85
- package/src/tools/registry.ts +0 -103
- package/src/tools/writeFileTool.ts +0 -167
- package/src/ui/BrandSplash.tsx +0 -133
- package/src/ui/terminalTitle.ts +0 -30
- package/src/utils/images.ts +0 -140
- package/src/utils/markdownSegments.ts +0 -51
- package/src/utils/messages.ts +0 -37
- package/src/utils/withRetry.ts +0 -324
- /package/src/identity/{hub → manager}/continuity/state.ts +0 -0
- /package/src/identity/{hub → manager}/custody/helpers.ts +0 -0
- /package/src/identity/{hub → manager}/custody/preflight.ts +0 -0
- /package/src/identity/{hub → manager}/custody/state.ts +0 -0
- /package/src/identity/{hub → manager}/custody/useCustodyFlow.tsx +0 -0
- /package/src/identity/{hub → manager}/ens/EnsEditFlow.tsx +0 -0
- /package/src/identity/{hub → manager}/ens/advancedEnsValidation.ts +0 -0
- /package/src/identity/{hub → manager}/ens/state.ts +0 -0
- /package/src/identity/{hub → manager}/ens/transactions.ts +0 -0
- /package/src/identity/{hub → manager}/ens/types.ts +0 -0
- /package/src/identity/{hub → manager}/profile/identity.ts +0 -0
- /package/src/identity/{hub → manager}/restore/envelopes.ts +0 -0
- /package/src/identity/{hub → manager}/restore/helpers.ts +0 -0
- /package/src/identity/{hub → manager}/restore/recovery.ts +0 -0
- /package/src/identity/{hub → manager}/restore/resolve.ts +0 -0
- /package/src/identity/{hub → manager}/shared/components/BusyScreen.tsx +0 -0
- /package/src/identity/{hub → manager}/shared/components/NetworkScreen.tsx +0 -0
- /package/src/identity/{hub → manager}/shared/components/PinataJwtInput.tsx +0 -0
- /package/src/identity/{hub → manager}/shared/effects/receipts.ts +0 -0
- /package/src/identity/{hub → manager}/shared/effects/sync.ts +0 -0
- /package/src/identity/{hub → manager}/shared/model/format.ts +0 -0
- /package/src/identity/{hub → manager}/shared/operatorWallets.ts +0 -0
- /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/ownership.ts +0 -0
- /package/src/identity/{hub → manager}/shared/reconciliation/agentReconciliation/types.ts +0 -0
- /package/src/identity/{hub → manager}/shared/reconciliation/walletSetup.ts +0 -0
- /package/src/identity/{hub → manager}/shared/txGuard.ts +0 -0
- /package/src/identity/{hub → manager}/transfer/progress.ts +0 -0
- /package/src/identity/{hub → manager}/transfer/state.ts +0 -0
|
@@ -1,712 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import { createHash } from 'node:crypto'
|
|
4
|
-
import {
|
|
5
|
-
findLocalHfModel,
|
|
6
|
-
getLocalHfCacheDir,
|
|
7
|
-
getLocalHfModelsPath,
|
|
8
|
-
loadLocalHfModels,
|
|
9
|
-
localPathFor,
|
|
10
|
-
saveLocalHfModels,
|
|
11
|
-
uninstallLocalHfModel,
|
|
12
|
-
upsertLocalHfModel,
|
|
13
|
-
} from './huggingfaceStorage.js'
|
|
14
|
-
|
|
15
|
-
export {
|
|
16
|
-
findLocalHfModel,
|
|
17
|
-
getLocalHfCacheDir,
|
|
18
|
-
getLocalHfModelsPath,
|
|
19
|
-
loadLocalHfModels,
|
|
20
|
-
saveLocalHfModels,
|
|
21
|
-
uninstallLocalHfModel,
|
|
22
|
-
upsertLocalHfModel,
|
|
23
|
-
} from './huggingfaceStorage.js'
|
|
24
|
-
|
|
25
|
-
export type HfFileFormat = 'gguf' | 'safetensors' | 'pickle/bin' | 'unknown'
|
|
26
|
-
export type HfRuntime = 'llama.cpp runnable' | 'download-only' | 'unsupported'
|
|
27
|
-
export type HfTask = 'chat/instruct' | 'base' | 'code' | 'embedding' | 'vision' | 'unknown'
|
|
28
|
-
export type HfSizeClass = 'tiny' | 'small' | 'medium' | 'large'
|
|
29
|
-
export type HfRisk = 'low' | 'medium' | 'high'
|
|
30
|
-
export type HfCredibility = 'established' | 'normal' | 'low-signal'
|
|
31
|
-
export type LocalHfStatus = 'ready' | 'incomplete'
|
|
32
|
-
|
|
33
|
-
export type HuggingFaceRef = {
|
|
34
|
-
repoId: string
|
|
35
|
-
revision?: string
|
|
36
|
-
filename?: string
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export type HuggingFaceSibling = {
|
|
40
|
-
filename: string
|
|
41
|
-
sizeBytes?: number
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export type HuggingFaceRepoInfo = {
|
|
45
|
-
repoId: string
|
|
46
|
-
author?: string
|
|
47
|
-
sha?: string
|
|
48
|
-
license?: string
|
|
49
|
-
downloads?: number
|
|
50
|
-
likes?: number
|
|
51
|
-
lastModified?: string
|
|
52
|
-
tags: string[]
|
|
53
|
-
siblings: HuggingFaceSibling[]
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export type HuggingFaceModelSearchItem = {
|
|
57
|
-
repoId: string
|
|
58
|
-
downloads?: number
|
|
59
|
-
likes?: number
|
|
60
|
-
lastModified?: string
|
|
61
|
-
tags: string[]
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export type HfSafetyReview = {
|
|
65
|
-
risk: HfRisk
|
|
66
|
-
credibility: HfCredibility
|
|
67
|
-
format: HfFileFormat
|
|
68
|
-
runtime: HfRuntime
|
|
69
|
-
task: HfTask
|
|
70
|
-
sizeClass: HfSizeClass
|
|
71
|
-
quantization?: string
|
|
72
|
-
reasons: string[]
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export type HfMmprojCandidate = {
|
|
76
|
-
filename: string
|
|
77
|
-
sizeBytes: number
|
|
78
|
-
localPath: string
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export type HfDownloadPlan = {
|
|
82
|
-
repo: HuggingFaceRepoInfo
|
|
83
|
-
repoId: string
|
|
84
|
-
requestedRevision: string
|
|
85
|
-
resolvedRevision: string
|
|
86
|
-
filename: string
|
|
87
|
-
sizeBytes: number
|
|
88
|
-
localPath: string
|
|
89
|
-
displayName: string
|
|
90
|
-
review: HfSafetyReview
|
|
91
|
-
mmprojCandidate?: HfMmprojCandidate
|
|
92
|
-
includeMmproj?: boolean
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export type LocalHfModel = {
|
|
96
|
-
id: string
|
|
97
|
-
provider: 'llamacpp'
|
|
98
|
-
repoId: string
|
|
99
|
-
requestedRevision: string
|
|
100
|
-
resolvedRevision: string
|
|
101
|
-
filename: string
|
|
102
|
-
displayName: string
|
|
103
|
-
localPath: string
|
|
104
|
-
sizeBytes: number
|
|
105
|
-
format: HfFileFormat
|
|
106
|
-
runtime: HfRuntime
|
|
107
|
-
task: HfTask
|
|
108
|
-
sizeClass: HfSizeClass
|
|
109
|
-
quantization?: string
|
|
110
|
-
risk: HfRisk
|
|
111
|
-
credibility: HfCredibility
|
|
112
|
-
license?: string
|
|
113
|
-
downloads?: number
|
|
114
|
-
likes?: number
|
|
115
|
-
reviewedAt: string
|
|
116
|
-
installedAt: string
|
|
117
|
-
status: LocalHfStatus
|
|
118
|
-
sha256?: string
|
|
119
|
-
mmprojPath?: string
|
|
120
|
-
mmprojAvailable?: boolean
|
|
121
|
-
mmprojSizeBytes?: number
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export type HfDownloadProgress = {
|
|
125
|
-
status: string
|
|
126
|
-
completed?: number
|
|
127
|
-
total?: number
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
type FetchImpl = typeof fetch
|
|
131
|
-
type ModelInfoResponse = {
|
|
132
|
-
id?: unknown
|
|
133
|
-
author?: unknown
|
|
134
|
-
sha?: unknown
|
|
135
|
-
downloads?: unknown
|
|
136
|
-
likes?: unknown
|
|
137
|
-
lastModified?: unknown
|
|
138
|
-
tags?: unknown
|
|
139
|
-
cardData?: unknown
|
|
140
|
-
siblings?: unknown
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
type ModelSearchResponseItem = {
|
|
144
|
-
id?: unknown
|
|
145
|
-
modelId?: unknown
|
|
146
|
-
downloads?: unknown
|
|
147
|
-
likes?: unknown
|
|
148
|
-
lastModified?: unknown
|
|
149
|
-
tags?: unknown
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const HF_BASE_URL = 'https://huggingface.co'
|
|
153
|
-
const DEFAULT_REVISION = 'main'
|
|
154
|
-
const COMMIT_RE = /^[a-f0-9]{40}$/i
|
|
155
|
-
const DOWNLOAD_PROGRESS_MIN_MS = 100
|
|
156
|
-
const DOWNLOAD_PROGRESS_MIN_BYTES = 16 * 1024 * 1024
|
|
157
|
-
|
|
158
|
-
export function parseHuggingFaceRef(input: string): HuggingFaceRef {
|
|
159
|
-
const trimmed = input.trim()
|
|
160
|
-
if (!trimmed) throw new Error('Hugging Face model link or repo id is required')
|
|
161
|
-
|
|
162
|
-
if (/^https?:\/\//i.test(trimmed)) {
|
|
163
|
-
const url = new URL(trimmed)
|
|
164
|
-
const host = url.hostname.toLowerCase()
|
|
165
|
-
if (host !== 'huggingface.co' && host !== 'www.huggingface.co') {
|
|
166
|
-
throw new Error('Expected a huggingface.co model link')
|
|
167
|
-
}
|
|
168
|
-
const parts = url.pathname.split('/').filter(Boolean)
|
|
169
|
-
if (parts.length < 2) throw new Error('Expected a Hugging Face repo link')
|
|
170
|
-
const repoId = `${decodeURIComponent(parts[0]!)}/${decodeURIComponent(parts[1]!)}`
|
|
171
|
-
const mode = parts[2]
|
|
172
|
-
if (mode === 'blob' || mode === 'resolve' || mode === 'tree') {
|
|
173
|
-
const revision = parts[3] ? decodeURIComponent(parts[3]) : undefined
|
|
174
|
-
const filename = parts.length > 4
|
|
175
|
-
? parts.slice(4).map(part => decodeURIComponent(part)).join('/')
|
|
176
|
-
: undefined
|
|
177
|
-
return { repoId, revision, filename }
|
|
178
|
-
}
|
|
179
|
-
return { repoId }
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const withoutPrefix = trimmed.replace(/^hf:\/\//i, '')
|
|
183
|
-
const parts = withoutPrefix.split('/').filter(Boolean)
|
|
184
|
-
if (parts.length < 2) throw new Error('Expected repo id like org/model or a huggingface.co link')
|
|
185
|
-
const repoId = `${parts[0]!}/${parts[1]!}`
|
|
186
|
-
let fileParts = parts.slice(2)
|
|
187
|
-
const mode = fileParts[0]
|
|
188
|
-
if ((mode === 'blob' || mode === 'resolve' || mode === 'tree') && fileParts.length >= 2) {
|
|
189
|
-
fileParts = fileParts.slice(2)
|
|
190
|
-
}
|
|
191
|
-
const filename = fileParts.length > 0 ? fileParts.join('/') : undefined
|
|
192
|
-
return { repoId, filename }
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export async function fetchHuggingFaceRepoInfo(
|
|
196
|
-
ref: HuggingFaceRef,
|
|
197
|
-
fetchImpl: FetchImpl = fetch,
|
|
198
|
-
): Promise<HuggingFaceRepoInfo> {
|
|
199
|
-
const url = new URL(`${HF_BASE_URL}/api/models/${encodeRepoPath(ref.repoId)}`)
|
|
200
|
-
url.searchParams.set('blobs', 'true')
|
|
201
|
-
if (ref.revision) url.searchParams.set('revision', ref.revision)
|
|
202
|
-
const response = await fetchImpl(url, { headers: { Accept: 'application/json' } })
|
|
203
|
-
if (!response.ok) {
|
|
204
|
-
if (response.status === 401 || response.status === 403) {
|
|
205
|
-
throw new Error('Repo is gated or private')
|
|
206
|
-
}
|
|
207
|
-
if (response.status === 404) throw new Error('Hugging Face repo not found')
|
|
208
|
-
throw new Error(`Hugging Face API HTTP ${response.status}`)
|
|
209
|
-
}
|
|
210
|
-
const data = await response.json() as ModelInfoResponse
|
|
211
|
-
const tags = Array.isArray(data.tags)
|
|
212
|
-
? data.tags.filter((tag): tag is string => typeof tag === 'string')
|
|
213
|
-
: []
|
|
214
|
-
const siblings = Array.isArray(data.siblings)
|
|
215
|
-
? data.siblings.flatMap(sibling => parseSibling(sibling))
|
|
216
|
-
: []
|
|
217
|
-
return {
|
|
218
|
-
repoId: typeof data.id === 'string' ? data.id : ref.repoId,
|
|
219
|
-
author: typeof data.author === 'string' ? data.author : undefined,
|
|
220
|
-
sha: typeof data.sha === 'string' ? data.sha : undefined,
|
|
221
|
-
license: licenseFrom(data.cardData, tags),
|
|
222
|
-
downloads: typeof data.downloads === 'number' ? data.downloads : undefined,
|
|
223
|
-
likes: typeof data.likes === 'number' ? data.likes : undefined,
|
|
224
|
-
lastModified: typeof data.lastModified === 'string' ? data.lastModified : undefined,
|
|
225
|
-
tags,
|
|
226
|
-
siblings,
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export async function searchHuggingFaceModels(
|
|
231
|
-
query: string,
|
|
232
|
-
options: { limit?: number; filter?: string } = {},
|
|
233
|
-
fetchImpl: FetchImpl = fetch,
|
|
234
|
-
): Promise<HuggingFaceModelSearchItem[]> {
|
|
235
|
-
const url = new URL(`${HF_BASE_URL}/api/models`)
|
|
236
|
-
url.searchParams.set('search', query)
|
|
237
|
-
url.searchParams.set('sort', 'downloads')
|
|
238
|
-
url.searchParams.set('direction', '-1')
|
|
239
|
-
url.searchParams.set('limit', String(options.limit ?? 20))
|
|
240
|
-
if (options.filter) url.searchParams.set('filter', options.filter)
|
|
241
|
-
const response = await fetchImpl(url, { headers: { Accept: 'application/json' } })
|
|
242
|
-
if (!response.ok) throw new Error(`Hugging Face catalog HTTP ${response.status}`)
|
|
243
|
-
const data = await response.json() as unknown
|
|
244
|
-
if (!Array.isArray(data)) return []
|
|
245
|
-
return data.flatMap(parseSearchItem)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export function ggufFiles(repo: HuggingFaceRepoInfo): HuggingFaceSibling[] {
|
|
249
|
-
return repo.siblings
|
|
250
|
-
.filter(file => file.filename.toLowerCase().endsWith('.gguf'))
|
|
251
|
-
.sort((a, b) => a.filename.localeCompare(b.filename))
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
export function isMmprojFilename(filename: string): boolean {
|
|
255
|
-
return filename.toLowerCase().startsWith('mmproj-') && filename.toLowerCase().endsWith('.gguf')
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export function findMmprojSibling(repo: HuggingFaceRepoInfo): HuggingFaceSibling | undefined {
|
|
259
|
-
return repo.siblings.find(file => isMmprojFilename(file.filename))
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
export async function createHfDownloadPlan(
|
|
263
|
-
input: string,
|
|
264
|
-
filename?: string,
|
|
265
|
-
deps: { fetchImpl?: FetchImpl; now?: () => Date } = {},
|
|
266
|
-
): Promise<HfDownloadPlan> {
|
|
267
|
-
const ref = parseHuggingFaceRef(input)
|
|
268
|
-
const selectedFilename = filename?.trim() || ref.filename
|
|
269
|
-
const repo = await fetchHuggingFaceRepoInfo(ref, deps.fetchImpl)
|
|
270
|
-
const files = ggufFiles(repo)
|
|
271
|
-
if (files.length === 0) {
|
|
272
|
-
throw new Error('No compatible local model files found for this link')
|
|
273
|
-
}
|
|
274
|
-
const selected = selectedFilename
|
|
275
|
-
? files.find(file => file.filename === selectedFilename)
|
|
276
|
-
: files[0]
|
|
277
|
-
if (!selected) {
|
|
278
|
-
throw new Error(`compatible file not found: ${selectedFilename}`)
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const requestedRevision = ref.revision ?? DEFAULT_REVISION
|
|
282
|
-
const resolvedRevision = repo.sha || requestedRevision
|
|
283
|
-
const sizeBytes = selected.sizeBytes ?? 0
|
|
284
|
-
const review = reviewHfModel({
|
|
285
|
-
repo,
|
|
286
|
-
filename: selected.filename,
|
|
287
|
-
sizeBytes,
|
|
288
|
-
requestedRevision,
|
|
289
|
-
resolvedRevision,
|
|
290
|
-
})
|
|
291
|
-
const mmprojSibling = findMmprojSibling(repo)
|
|
292
|
-
const mmprojCandidate: HfMmprojCandidate | undefined = mmprojSibling
|
|
293
|
-
? {
|
|
294
|
-
filename: mmprojSibling.filename,
|
|
295
|
-
sizeBytes: mmprojSibling.sizeBytes ?? 0,
|
|
296
|
-
localPath: localPathFor(repo.repoId, resolvedRevision, mmprojSibling.filename),
|
|
297
|
-
}
|
|
298
|
-
: undefined
|
|
299
|
-
return {
|
|
300
|
-
repo,
|
|
301
|
-
repoId: repo.repoId,
|
|
302
|
-
requestedRevision,
|
|
303
|
-
resolvedRevision,
|
|
304
|
-
filename: selected.filename,
|
|
305
|
-
sizeBytes,
|
|
306
|
-
localPath: localPathFor(repo.repoId, resolvedRevision, selected.filename),
|
|
307
|
-
displayName: displayNameFor(repo.repoId, selected.filename),
|
|
308
|
-
review,
|
|
309
|
-
mmprojCandidate,
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
export function reviewHfModel(args: {
|
|
314
|
-
repo: HuggingFaceRepoInfo
|
|
315
|
-
filename: string
|
|
316
|
-
sizeBytes: number
|
|
317
|
-
requestedRevision: string
|
|
318
|
-
resolvedRevision: string
|
|
319
|
-
}): HfSafetyReview {
|
|
320
|
-
const format = fileFormat(args.filename)
|
|
321
|
-
const runtime: HfRuntime =
|
|
322
|
-
format === 'gguf' ? 'llama.cpp runnable'
|
|
323
|
-
: format === 'safetensors' ? 'download-only'
|
|
324
|
-
: 'unsupported'
|
|
325
|
-
const quantization = quantizationFromFilename(args.filename)
|
|
326
|
-
const task = taskFor(args.repo, args.filename)
|
|
327
|
-
const sizeClass = sizeClassFor(args.sizeBytes)
|
|
328
|
-
const credibility = credibilityFor(args.repo)
|
|
329
|
-
const pinned = COMMIT_RE.test(args.requestedRevision) || COMMIT_RE.test(args.resolvedRevision)
|
|
330
|
-
const repoHasRiskyFiles = args.repo.siblings.some(file => fileFormat(file.filename) === 'pickle/bin')
|
|
331
|
-
const reasons: string[] = []
|
|
332
|
-
|
|
333
|
-
if (format !== 'gguf') reasons.push('selected file is not compatible with local chat')
|
|
334
|
-
if (!pinned) reasons.push('revision is mutable')
|
|
335
|
-
if (!args.repo.license) reasons.push('license is missing or unknown')
|
|
336
|
-
if (credibility === 'low-signal') reasons.push('repo has limited public usage signals')
|
|
337
|
-
if (repoHasRiskyFiles) reasons.push('repo also contains pickle/bin model files')
|
|
338
|
-
|
|
339
|
-
const risk: HfRisk =
|
|
340
|
-
format !== 'gguf'
|
|
341
|
-
? 'high'
|
|
342
|
-
: repoHasRiskyFiles
|
|
343
|
-
? 'medium'
|
|
344
|
-
: pinned && args.repo.license && credibility !== 'low-signal'
|
|
345
|
-
? 'low'
|
|
346
|
-
: 'medium'
|
|
347
|
-
|
|
348
|
-
if (reasons.length === 0) reasons.push('compatible local model file with usable repo metadata')
|
|
349
|
-
|
|
350
|
-
return {
|
|
351
|
-
risk,
|
|
352
|
-
credibility,
|
|
353
|
-
format,
|
|
354
|
-
runtime,
|
|
355
|
-
task,
|
|
356
|
-
sizeClass,
|
|
357
|
-
quantization,
|
|
358
|
-
reasons,
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
export async function* downloadHfModel(
|
|
363
|
-
plan: HfDownloadPlan,
|
|
364
|
-
signal?: AbortSignal,
|
|
365
|
-
fetchImpl: FetchImpl = fetch,
|
|
366
|
-
): AsyncIterable<HfDownloadProgress> {
|
|
367
|
-
if (plan.review.runtime !== 'llama.cpp runnable') {
|
|
368
|
-
throw new Error('Selected file is not compatible with local chat')
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
await fs.mkdir(path.dirname(plan.localPath), { recursive: true })
|
|
372
|
-
const partialPath = `${plan.localPath}.partial`
|
|
373
|
-
const response = await fetchImpl(resolveUrl(plan.repoId, plan.resolvedRevision, plan.filename), { signal })
|
|
374
|
-
if (!response.ok || !response.body) {
|
|
375
|
-
throw new Error(response.ok ? 'empty download body' : `download HTTP ${response.status}`)
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const total = Number.parseInt(response.headers.get('content-length') ?? '', 10)
|
|
379
|
-
const hash = createHash('sha256')
|
|
380
|
-
const handle = await fs.open(partialPath, 'w')
|
|
381
|
-
let completed = 0
|
|
382
|
-
let complete = false
|
|
383
|
-
let lastProgressAt = Date.now()
|
|
384
|
-
let lastProgressBytes = 0
|
|
385
|
-
yield { status: 'starting', completed, total: Number.isFinite(total) ? total : undefined }
|
|
386
|
-
try {
|
|
387
|
-
const reader = response.body.getReader()
|
|
388
|
-
while (true) {
|
|
389
|
-
const { done, value } = await reader.read()
|
|
390
|
-
if (done) break
|
|
391
|
-
if (signal?.aborted) throw new Error('Cancelled')
|
|
392
|
-
const buffer = Buffer.from(value)
|
|
393
|
-
hash.update(buffer)
|
|
394
|
-
await handle.write(buffer)
|
|
395
|
-
completed += buffer.byteLength
|
|
396
|
-
const now = Date.now()
|
|
397
|
-
if (shouldReportDownloadProgress(completed, lastProgressBytes, now, lastProgressAt)) {
|
|
398
|
-
lastProgressAt = now
|
|
399
|
-
lastProgressBytes = completed
|
|
400
|
-
yield { status: 'downloading', completed, total: Number.isFinite(total) ? total : undefined }
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
complete = true
|
|
404
|
-
} finally {
|
|
405
|
-
await handle.close()
|
|
406
|
-
if (!complete) {
|
|
407
|
-
await fs.unlink(partialPath).catch(() => {})
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
await fs.rename(partialPath, plan.localPath)
|
|
412
|
-
|
|
413
|
-
let mmprojPath: string | undefined
|
|
414
|
-
if (plan.includeMmproj && plan.mmprojCandidate) {
|
|
415
|
-
yield* downloadMmprojFile(plan.repoId, plan.resolvedRevision, plan.mmprojCandidate, signal, fetchImpl)
|
|
416
|
-
mmprojPath = plan.mmprojCandidate.localPath
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
await upsertLocalHfModel(modelFromPlan(plan, hash.digest('hex'), 'ready', mmprojPath))
|
|
420
|
-
yield { status: 'success', completed, total: Number.isFinite(total) ? total : completed }
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
async function* downloadMmprojFile(
|
|
424
|
-
repoId: string,
|
|
425
|
-
resolvedRevision: string,
|
|
426
|
-
candidate: HfMmprojCandidate,
|
|
427
|
-
signal: AbortSignal | undefined,
|
|
428
|
-
fetchImpl: FetchImpl,
|
|
429
|
-
): AsyncIterable<HfDownloadProgress> {
|
|
430
|
-
await fs.mkdir(path.dirname(candidate.localPath), { recursive: true })
|
|
431
|
-
const partialPath = `${candidate.localPath}.partial`
|
|
432
|
-
const response = await fetchImpl(resolveUrl(repoId, resolvedRevision, candidate.filename), { signal })
|
|
433
|
-
if (!response.ok || !response.body) {
|
|
434
|
-
throw new Error(response.ok ? 'empty projector download body' : `projector download HTTP ${response.status}`)
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const total = Number.parseInt(response.headers.get('content-length') ?? '', 10)
|
|
438
|
-
const handle = await fs.open(partialPath, 'w')
|
|
439
|
-
let completed = 0
|
|
440
|
-
let complete = false
|
|
441
|
-
let lastProgressAt = Date.now()
|
|
442
|
-
let lastProgressBytes = 0
|
|
443
|
-
yield { status: 'downloading-mmproj', completed, total: Number.isFinite(total) ? total : undefined }
|
|
444
|
-
try {
|
|
445
|
-
const reader = response.body.getReader()
|
|
446
|
-
while (true) {
|
|
447
|
-
const { done, value } = await reader.read()
|
|
448
|
-
if (done) break
|
|
449
|
-
if (signal?.aborted) throw new Error('Cancelled')
|
|
450
|
-
const buffer = Buffer.from(value)
|
|
451
|
-
await handle.write(buffer)
|
|
452
|
-
completed += buffer.byteLength
|
|
453
|
-
const now = Date.now()
|
|
454
|
-
if (shouldReportDownloadProgress(completed, lastProgressBytes, now, lastProgressAt)) {
|
|
455
|
-
lastProgressAt = now
|
|
456
|
-
lastProgressBytes = completed
|
|
457
|
-
yield { status: 'downloading-mmproj', completed, total: Number.isFinite(total) ? total : undefined }
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
complete = true
|
|
461
|
-
} finally {
|
|
462
|
-
await handle.close()
|
|
463
|
-
if (!complete) {
|
|
464
|
-
await fs.unlink(partialPath).catch(() => {})
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
await fs.rename(partialPath, candidate.localPath)
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
export async function backfillMmprojAvailability(
|
|
472
|
-
model: LocalHfModel,
|
|
473
|
-
fetchImpl: FetchImpl = fetch,
|
|
474
|
-
): Promise<LocalHfModel> {
|
|
475
|
-
if (model.mmprojAvailable !== undefined) return model
|
|
476
|
-
try {
|
|
477
|
-
const repo = await fetchHuggingFaceRepoInfo({ repoId: model.repoId }, fetchImpl)
|
|
478
|
-
const sibling = findMmprojSibling(repo)
|
|
479
|
-
const next: LocalHfModel = {
|
|
480
|
-
...model,
|
|
481
|
-
mmprojAvailable: Boolean(sibling),
|
|
482
|
-
mmprojSizeBytes: sibling?.sizeBytes,
|
|
483
|
-
}
|
|
484
|
-
await upsertLocalHfModel(next)
|
|
485
|
-
return next
|
|
486
|
-
} catch {
|
|
487
|
-
return model
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
export async function backfillMmprojForModels(
|
|
492
|
-
models: LocalHfModel[],
|
|
493
|
-
fetchImpl: FetchImpl = fetch,
|
|
494
|
-
): Promise<LocalHfModel[]> {
|
|
495
|
-
const repoIdToProbe = new Map<string, Promise<HuggingFaceRepoInfo | null>>()
|
|
496
|
-
for (const model of models) {
|
|
497
|
-
if (model.mmprojAvailable !== undefined) continue
|
|
498
|
-
if (repoIdToProbe.has(model.repoId)) continue
|
|
499
|
-
repoIdToProbe.set(
|
|
500
|
-
model.repoId,
|
|
501
|
-
fetchHuggingFaceRepoInfo({ repoId: model.repoId }, fetchImpl).catch(() => null),
|
|
502
|
-
)
|
|
503
|
-
}
|
|
504
|
-
if (repoIdToProbe.size === 0) return models
|
|
505
|
-
const resolved = new Map<string, HuggingFaceRepoInfo | null>()
|
|
506
|
-
for (const [repoId, promise] of repoIdToProbe) {
|
|
507
|
-
resolved.set(repoId, await promise)
|
|
508
|
-
}
|
|
509
|
-
const out: LocalHfModel[] = []
|
|
510
|
-
for (const model of models) {
|
|
511
|
-
if (model.mmprojAvailable !== undefined) {
|
|
512
|
-
out.push(model)
|
|
513
|
-
continue
|
|
514
|
-
}
|
|
515
|
-
const repo = resolved.get(model.repoId)
|
|
516
|
-
if (!repo) {
|
|
517
|
-
out.push(model)
|
|
518
|
-
continue
|
|
519
|
-
}
|
|
520
|
-
const sibling = findMmprojSibling(repo)
|
|
521
|
-
const next: LocalHfModel = {
|
|
522
|
-
...model,
|
|
523
|
-
mmprojAvailable: Boolean(sibling),
|
|
524
|
-
mmprojSizeBytes: sibling?.sizeBytes,
|
|
525
|
-
}
|
|
526
|
-
await upsertLocalHfModel(next)
|
|
527
|
-
out.push(next)
|
|
528
|
-
}
|
|
529
|
-
return out
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
export async function* addMmprojToInstalledModel(
|
|
533
|
-
modelId: string,
|
|
534
|
-
signal?: AbortSignal,
|
|
535
|
-
deps: { fetchImpl?: FetchImpl } = {},
|
|
536
|
-
): AsyncIterable<HfDownloadProgress> {
|
|
537
|
-
const fetchImpl = deps.fetchImpl ?? fetch
|
|
538
|
-
const existing = await findLocalHfModel(modelId)
|
|
539
|
-
if (!existing) throw new Error(`model not installed: ${modelId}`)
|
|
540
|
-
if (existing.mmprojPath) {
|
|
541
|
-
yield { status: 'success', completed: 0 }
|
|
542
|
-
return
|
|
543
|
-
}
|
|
544
|
-
const repo = await fetchHuggingFaceRepoInfo({ repoId: existing.repoId }, fetchImpl)
|
|
545
|
-
const sibling = findMmprojSibling(repo)
|
|
546
|
-
if (!sibling) throw new Error(`no vision encoder available for ${existing.repoId}`)
|
|
547
|
-
const candidate: HfMmprojCandidate = {
|
|
548
|
-
filename: sibling.filename,
|
|
549
|
-
sizeBytes: sibling.sizeBytes ?? 0,
|
|
550
|
-
localPath: localPathFor(existing.repoId, existing.resolvedRevision, sibling.filename),
|
|
551
|
-
}
|
|
552
|
-
yield* downloadMmprojFile(existing.repoId, existing.resolvedRevision, candidate, signal, fetchImpl)
|
|
553
|
-
await upsertLocalHfModel({ ...existing, mmprojPath: candidate.localPath })
|
|
554
|
-
yield { status: 'success', completed: candidate.sizeBytes }
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
export function shouldReportDownloadProgress(
|
|
558
|
-
completed: number,
|
|
559
|
-
lastCompleted: number,
|
|
560
|
-
nowMs: number,
|
|
561
|
-
lastReportedMs: number,
|
|
562
|
-
): boolean {
|
|
563
|
-
return nowMs - lastReportedMs >= DOWNLOAD_PROGRESS_MIN_MS
|
|
564
|
-
|| completed - lastCompleted >= DOWNLOAD_PROGRESS_MIN_BYTES
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
export function modelFromPlan(
|
|
568
|
-
plan: HfDownloadPlan,
|
|
569
|
-
sha256: string | undefined,
|
|
570
|
-
status: LocalHfStatus,
|
|
571
|
-
mmprojPath?: string,
|
|
572
|
-
): LocalHfModel {
|
|
573
|
-
const mmprojAvailable = Boolean(plan.mmprojCandidate)
|
|
574
|
-
const now = new Date().toISOString()
|
|
575
|
-
return {
|
|
576
|
-
id: localModelId(plan.repoId, plan.filename),
|
|
577
|
-
provider: 'llamacpp',
|
|
578
|
-
repoId: plan.repoId,
|
|
579
|
-
requestedRevision: plan.requestedRevision,
|
|
580
|
-
resolvedRevision: plan.resolvedRevision,
|
|
581
|
-
filename: plan.filename,
|
|
582
|
-
displayName: plan.displayName,
|
|
583
|
-
localPath: plan.localPath,
|
|
584
|
-
sizeBytes: plan.sizeBytes,
|
|
585
|
-
format: plan.review.format,
|
|
586
|
-
runtime: plan.review.runtime,
|
|
587
|
-
task: plan.review.task,
|
|
588
|
-
sizeClass: plan.review.sizeClass,
|
|
589
|
-
quantization: plan.review.quantization,
|
|
590
|
-
risk: plan.review.risk,
|
|
591
|
-
credibility: plan.review.credibility,
|
|
592
|
-
license: plan.repo.license,
|
|
593
|
-
downloads: plan.repo.downloads,
|
|
594
|
-
likes: plan.repo.likes,
|
|
595
|
-
reviewedAt: now,
|
|
596
|
-
installedAt: now,
|
|
597
|
-
status,
|
|
598
|
-
sha256,
|
|
599
|
-
mmprojPath,
|
|
600
|
-
mmprojAvailable,
|
|
601
|
-
mmprojSizeBytes: plan.mmprojCandidate?.sizeBytes,
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
export function localModelId(repoId: string, filename: string): string {
|
|
606
|
-
return `${repoId}#${filename}`
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
export function displayNameFor(repoId: string, filename: string): string {
|
|
610
|
-
const basename = filename.split('/').pop() ?? filename
|
|
611
|
-
return `${repoId} / ${basename}`
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
export function fileFormat(filename: string): HfFileFormat {
|
|
615
|
-
const lower = filename.toLowerCase()
|
|
616
|
-
if (lower.endsWith('.gguf')) return 'gguf'
|
|
617
|
-
if (lower.endsWith('.safetensors')) return 'safetensors'
|
|
618
|
-
if (/\.(bin|pt|pth|pkl|pickle)$/.test(lower)) return 'pickle/bin'
|
|
619
|
-
return 'unknown'
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
export function quantizationFromFilename(filename: string): string | undefined {
|
|
623
|
-
const match = filename.toUpperCase().match(/(?:^|[-_.])((?:IQ|Q)\d(?:_[A-Z0-9]+)*|F16|BF16|F32)(?=$|[-_.])/)
|
|
624
|
-
return match?.[1]
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
function resolveUrl(repoId: string, revision: string, filename: string): string {
|
|
628
|
-
return `${HF_BASE_URL}/${encodeRepoPath(repoId)}/resolve/${encodeURIComponent(revision)}/${encodePath(filename)}?download=true`
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
function encodeRepoPath(repoId: string): string {
|
|
632
|
-
return repoId.split('/').map(part => encodeURIComponent(part)).join('/')
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function encodePath(value: string): string {
|
|
636
|
-
return value.split('/').map(part => encodeURIComponent(part)).join('/')
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
function parseSibling(value: unknown): HuggingFaceSibling[] {
|
|
640
|
-
if (!value || typeof value !== 'object') return []
|
|
641
|
-
const record = value as { rfilename?: unknown; filename?: unknown; size?: unknown; lfs?: unknown }
|
|
642
|
-
const filename = typeof record.rfilename === 'string'
|
|
643
|
-
? record.rfilename
|
|
644
|
-
: typeof record.filename === 'string' ? record.filename : ''
|
|
645
|
-
if (!filename) return []
|
|
646
|
-
return [{
|
|
647
|
-
filename,
|
|
648
|
-
sizeBytes: siblingSizeBytes(record),
|
|
649
|
-
}]
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function parseSearchItem(value: unknown): HuggingFaceModelSearchItem[] {
|
|
653
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) return []
|
|
654
|
-
const item = value as ModelSearchResponseItem
|
|
655
|
-
const repoId = typeof item.id === 'string'
|
|
656
|
-
? item.id
|
|
657
|
-
: typeof item.modelId === 'string' ? item.modelId : ''
|
|
658
|
-
if (!repoId.includes('/')) return []
|
|
659
|
-
return [{
|
|
660
|
-
repoId,
|
|
661
|
-
downloads: typeof item.downloads === 'number' ? item.downloads : undefined,
|
|
662
|
-
likes: typeof item.likes === 'number' ? item.likes : undefined,
|
|
663
|
-
lastModified: typeof item.lastModified === 'string' ? item.lastModified : undefined,
|
|
664
|
-
tags: Array.isArray(item.tags)
|
|
665
|
-
? item.tags.filter((tag): tag is string => typeof tag === 'string')
|
|
666
|
-
: [],
|
|
667
|
-
}]
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
function siblingSizeBytes(record: { size?: unknown; lfs?: unknown }): number | undefined {
|
|
671
|
-
if (typeof record.size === 'number' && Number.isFinite(record.size) && record.size >= 0) {
|
|
672
|
-
return record.size
|
|
673
|
-
}
|
|
674
|
-
if (!record.lfs || typeof record.lfs !== 'object' || Array.isArray(record.lfs)) return undefined
|
|
675
|
-
const size = (record.lfs as { size?: unknown }).size
|
|
676
|
-
return typeof size === 'number' && Number.isFinite(size) && size >= 0 ? size : undefined
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function licenseFrom(cardData: unknown, tags: string[]): string | undefined {
|
|
680
|
-
if (cardData && typeof cardData === 'object' && !Array.isArray(cardData)) {
|
|
681
|
-
const license = (cardData as { license?: unknown }).license
|
|
682
|
-
if (typeof license === 'string' && license.trim()) return license.trim()
|
|
683
|
-
}
|
|
684
|
-
const tag = tags.find(item => item.startsWith('license:'))
|
|
685
|
-
return tag ? tag.slice('license:'.length) : undefined
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function credibilityFor(repo: HuggingFaceRepoInfo): HfCredibility {
|
|
689
|
-
const downloads = repo.downloads ?? 0
|
|
690
|
-
const likes = repo.likes ?? 0
|
|
691
|
-
if (downloads >= 10_000 || likes >= 100) return 'established'
|
|
692
|
-
if (downloads >= 100 || likes >= 5 || Boolean(repo.license)) return 'normal'
|
|
693
|
-
return 'low-signal'
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
function taskFor(repo: HuggingFaceRepoInfo, filename: string): HfTask {
|
|
697
|
-
const haystack = [repo.repoId, filename, ...repo.tags].join(' ').toLowerCase()
|
|
698
|
-
if (/(embed|embedding)/.test(haystack)) return 'embedding'
|
|
699
|
-
if (/(vision|vlm|multimodal)/.test(haystack)) return 'vision'
|
|
700
|
-
if (/(code|coder|coding)/.test(haystack)) return 'code'
|
|
701
|
-
if (/(chat|instruct|assistant)/.test(haystack)) return 'chat/instruct'
|
|
702
|
-
if (/(base)/.test(haystack)) return 'base'
|
|
703
|
-
return 'unknown'
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
function sizeClassFor(sizeBytes: number): HfSizeClass {
|
|
707
|
-
const gb = sizeBytes / 1e9
|
|
708
|
-
if (gb < 2) return 'tiny'
|
|
709
|
-
if (gb < 8) return 'small'
|
|
710
|
-
if (gb < 24) return 'medium'
|
|
711
|
-
return 'large'
|
|
712
|
-
}
|