ethagent 3.0.1 → 3.1.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/README.md +6 -1
- package/package.json +3 -1
- package/src/app/FirstRun.tsx +1 -24
- package/src/app/firstRunConfig.ts +26 -0
- package/src/auth/openaiOAuth/landingPage.ts +2 -11
- package/src/chat/ChatScreen.tsx +32 -117
- package/src/chat/MessageList.tsx +18 -260
- package/src/chat/chatEnvironment.ts +16 -0
- package/src/chat/chatTurnContext.ts +50 -0
- package/src/chat/chatTurnOrchestrator.ts +5 -112
- package/src/chat/chatTurnRows.ts +64 -0
- package/src/chat/commands.ts +3 -178
- package/src/chat/continuityEditReview.ts +42 -0
- package/src/chat/input/ChatInput.tsx +10 -144
- package/src/chat/input/chatInputHelpers.ts +62 -0
- package/src/chat/input/inputRendering.tsx +93 -0
- package/src/chat/messageMarkdown.ts +220 -0
- package/src/chat/messageRows.ts +43 -0
- package/src/chat/planImplementation.ts +62 -0
- package/src/chat/slashCommandHandlers.ts +165 -0
- package/src/chat/slashCommandViews.ts +120 -0
- package/src/cli/main.tsx +7 -0
- package/src/identity/continuity/challenges.ts +123 -0
- package/src/identity/continuity/envelope.ts +49 -1484
- package/src/identity/continuity/envelopeCreate.ts +322 -0
- package/src/identity/continuity/envelopeCrypto.ts +182 -0
- package/src/identity/continuity/envelopeParse.ts +441 -0
- package/src/identity/continuity/envelopeTypes.ts +204 -0
- package/src/identity/continuity/envelopeVersion.ts +1 -0
- package/src/identity/continuity/payloadNormalization.ts +183 -0
- package/src/identity/continuity/publicSkills.ts +5 -5
- package/src/identity/continuity/skills/loadSkills.ts +12 -69
- package/src/identity/continuity/skills/skillPaths.ts +76 -0
- package/src/identity/continuity/skillsNormalization.ts +119 -0
- package/src/identity/continuity/snapshotToken.ts +28 -0
- package/src/identity/hub/continuity/completion.ts +67 -0
- package/src/identity/hub/continuity/effects.ts +5 -62
- package/src/identity/hub/profile/effects.ts +6 -170
- package/src/identity/hub/profile/operatorSave.ts +202 -0
- package/src/identity/registry/erc8004/metadata.ts +31 -23
- package/src/identity/wallet/browserWallet/html.ts +1 -57
- package/src/identity/wallet/browserWallet/walletPageSource.ts +85 -0
- package/src/identity/wallet/page/controller.ts +1 -1
- package/src/identity/wallet/page/errorView.ts +122 -0
- package/src/identity/wallet/page/view.ts +3 -114
- package/src/mcp/manager.ts +8 -66
- package/src/mcp/managerHelpers.ts +70 -0
- package/src/models/ModelPicker.tsx +69 -889
- package/src/models/huggingface.ts +20 -137
- package/src/models/huggingfaceStorage.ts +136 -0
- package/src/models/llamacpp.ts +37 -303
- package/src/models/llamacppCommands.ts +44 -0
- package/src/models/llamacppConfig.ts +34 -0
- package/src/models/llamacppDiscovery.ts +176 -0
- package/src/models/llamacppOutput.ts +65 -0
- package/src/models/modelPickerCatalogFlow.ts +56 -0
- package/src/models/modelPickerCredentials.ts +166 -0
- package/src/models/modelPickerData.ts +41 -0
- package/src/models/modelPickerDisplay.tsx +132 -0
- package/src/models/modelPickerHfFlow.ts +192 -0
- package/src/models/modelPickerLocalRunnerFlow.ts +115 -0
- package/src/models/modelPickerTypes.ts +69 -0
- package/src/models/modelPickerUninstallFlow.ts +48 -0
- package/src/models/modelPickerViewHelpers.ts +174 -0
- package/src/providers/openai-chat.ts +5 -124
- package/src/providers/openaiChatWire.ts +124 -0
- package/src/runtime/providerTurn.ts +38 -0
- package/src/runtime/textToolParser.ts +161 -0
- package/src/runtime/toolIntent.ts +1 -1
- package/src/runtime/turn.ts +43 -499
- package/src/runtime/turnNudges.ts +223 -0
- package/src/runtime/turnTypes.ts +86 -0
- package/src/ui/terminalTitle.ts +30 -0
|
@@ -9,12 +9,8 @@ import { theme } from '../ui/theme.js'
|
|
|
9
9
|
import {
|
|
10
10
|
DEFAULT_LLAMA_HOST,
|
|
11
11
|
detectLlamaCpp,
|
|
12
|
-
installLlamaCppRunner,
|
|
13
12
|
killRogueLlamaProcesses,
|
|
14
|
-
setLlamaCppServerPath,
|
|
15
|
-
startLlamaCppServer,
|
|
16
13
|
stopLlamaCppServer,
|
|
17
|
-
type LlamaCppInstallProgress,
|
|
18
14
|
type LlamaCppInstallResult,
|
|
19
15
|
type LlamaCppStartResult,
|
|
20
16
|
} from './llamacpp.js'
|
|
@@ -31,23 +27,13 @@ import { hasOpenAIOAuthCredentials, rmOpenAIOAuthCredentials } from '../auth/ope
|
|
|
31
27
|
import { openExternalUrl } from '../utils/openExternal.js'
|
|
32
28
|
import { defaultModelFor, type EthagentConfig, type ProviderId } from '../storage/config.js'
|
|
33
29
|
import { clearModelCatalogCache, discoverProviderModels, isOpenAIOAuthAllowedModel, OPENAI_OAUTH_DEFAULT_MODEL, type ModelCatalogResult } from './catalog.js'
|
|
34
|
-
import { contextWindowInfo } from '../runtime/compaction.js'
|
|
35
30
|
import {
|
|
36
|
-
addMmprojToInstalledModel,
|
|
37
|
-
backfillMmprojForModels,
|
|
38
31
|
createHfDownloadPlan,
|
|
39
|
-
downloadHfModel,
|
|
40
|
-
fetchHuggingFaceRepoInfo,
|
|
41
32
|
findLocalHfModel,
|
|
42
33
|
ggufFiles,
|
|
43
34
|
loadLocalHfModels,
|
|
44
35
|
localModelId,
|
|
45
|
-
modelFromPlan,
|
|
46
|
-
parseHuggingFaceRef,
|
|
47
|
-
uninstallLocalHfModel,
|
|
48
36
|
type HfCredibility,
|
|
49
|
-
type HfDownloadPlan,
|
|
50
|
-
type HfDownloadProgress,
|
|
51
37
|
type HfRisk,
|
|
52
38
|
type HuggingFaceRepoInfo,
|
|
53
39
|
type HuggingFaceSibling,
|
|
@@ -69,57 +55,75 @@ import {
|
|
|
69
55
|
} from './modelPickerOptions.js'
|
|
70
56
|
import { formatLocalHfModelDisplayName, formatModelDisplayName } from './modelDisplay.js'
|
|
71
57
|
import { fetchUncensoredGgufCatalog, type UncensoredCatalogEntry } from './uncensoredCatalog.js'
|
|
58
|
+
import type {
|
|
59
|
+
LoadedModelPickerData as LoadedData,
|
|
60
|
+
LocalUninstallTarget,
|
|
61
|
+
ModelPickerProps,
|
|
62
|
+
ModelPickerSelection,
|
|
63
|
+
ModelPickerState as State,
|
|
64
|
+
} from './modelPickerTypes.js'
|
|
65
|
+
import {
|
|
66
|
+
ElapsedSpinner,
|
|
67
|
+
contextFitLabel,
|
|
68
|
+
contextFitSubtitle,
|
|
69
|
+
credibilityLabel,
|
|
70
|
+
fitColor,
|
|
71
|
+
fitLabel,
|
|
72
|
+
formatBytes,
|
|
73
|
+
formatSignals,
|
|
74
|
+
friendlyFileName,
|
|
75
|
+
friendlyReasons,
|
|
76
|
+
isCloudProvider,
|
|
77
|
+
modelMetadataSubtext,
|
|
78
|
+
providerKeyPlaceholder,
|
|
79
|
+
riskColor,
|
|
80
|
+
runnerPathPlaceholder,
|
|
81
|
+
safetyLabel,
|
|
82
|
+
} from './modelPickerDisplay.js'
|
|
83
|
+
import {
|
|
84
|
+
buildCatalogOptions,
|
|
85
|
+
buildHfFileOptions,
|
|
86
|
+
buildRunnerRecoveryOptions,
|
|
87
|
+
configForProvider,
|
|
88
|
+
localModelOptionIndex,
|
|
89
|
+
localOrCloudOptionIndex,
|
|
90
|
+
parseCloudValue,
|
|
91
|
+
parseFullCatalogValue,
|
|
92
|
+
parseKeyValue,
|
|
93
|
+
pickFallbackSelection,
|
|
94
|
+
} from './modelPickerViewHelpers.js'
|
|
95
|
+
import { openLocalCatalog, reviewCatalogModel } from './modelPickerCatalogFlow.js'
|
|
96
|
+
import { loadHfPickerModels, probeLlamaCpp } from './modelPickerData.js'
|
|
97
|
+
import {
|
|
98
|
+
chooseInstalledHfModelForRepo,
|
|
99
|
+
downloadMmprojAndContinue,
|
|
100
|
+
findInstalledHfModelForInput,
|
|
101
|
+
inspectHfInput,
|
|
102
|
+
reviewHfFile,
|
|
103
|
+
startHfDownload,
|
|
104
|
+
} from './modelPickerHfFlow.js'
|
|
105
|
+
import {
|
|
106
|
+
installRunnerAndStart,
|
|
107
|
+
localRunnerStartFailureSubtitle,
|
|
108
|
+
runRunnerSetup,
|
|
109
|
+
saveRunnerPathAndStart,
|
|
110
|
+
startAndPickHfModel,
|
|
111
|
+
} from './modelPickerLocalRunnerFlow.js'
|
|
112
|
+
import {
|
|
113
|
+
isCurrentLocalUninstallTarget,
|
|
114
|
+
localUninstallBoundaryCopy,
|
|
115
|
+
localUninstallTargets,
|
|
116
|
+
uninstallLocalModel,
|
|
117
|
+
} from './modelPickerUninstallFlow.js'
|
|
118
|
+
import {
|
|
119
|
+
deleteKey,
|
|
120
|
+
signOutOAuth,
|
|
121
|
+
startOpenAIOAuthFlow,
|
|
122
|
+
submitKey,
|
|
123
|
+
} from './modelPickerCredentials.js'
|
|
72
124
|
|
|
73
|
-
export type ModelPickerSelection
|
|
74
|
-
|
|
75
|
-
| { kind: 'cloud'; provider: CloudProviderId; model: string; keyJustSet: boolean }
|
|
76
|
-
|
|
77
|
-
type ModelPickerProps = {
|
|
78
|
-
currentConfig: EthagentConfig
|
|
79
|
-
currentProvider: ProviderId
|
|
80
|
-
currentModel: string
|
|
81
|
-
contextFit?: ModelPickerContextFit | null
|
|
82
|
-
featuredHfRepo?: string
|
|
83
|
-
localOnly?: boolean
|
|
84
|
-
onPick: (selection: ModelPickerSelection) => void
|
|
85
|
-
onCancel: () => void
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
type LoadedData = ModelPickerOptionsData
|
|
89
|
-
type LocalUninstallTarget = { kind: 'hf'; id: string; displayName: string; sizeBytes: number }
|
|
90
|
-
|
|
91
|
-
type State =
|
|
92
|
-
| { kind: 'loading' }
|
|
93
|
-
| { kind: 'list'; data: LoadedData }
|
|
94
|
-
| { kind: 'localCatalogLoading'; data: LoadedData }
|
|
95
|
-
| { kind: 'localCatalog'; data: LoadedData; catalog: UncensoredCatalogEntry[] }
|
|
96
|
-
| { kind: 'localCatalogError'; data: LoadedData; message: string }
|
|
97
|
-
| { kind: 'catalog'; provider: CloudProviderId; data: LoadedData }
|
|
98
|
-
| { kind: 'keyEntry'; provider: CloudProviderId; action: 'set' | 'edit'; data: LoadedData; submitting: boolean; error?: string }
|
|
99
|
-
| { kind: 'keyManage'; provider: CloudProviderId; data: LoadedData; submitting: boolean; error?: string }
|
|
100
|
-
| { kind: 'oauthManage'; data: LoadedData; submitting: boolean; error?: string }
|
|
101
|
-
| { kind: 'oauthLogin'; data: LoadedData; phase: 'waiting' | 'exchanging' | 'error'; url?: string; message?: string }
|
|
102
|
-
| { kind: 'hfInput'; data: LoadedData; error?: string }
|
|
103
|
-
| { kind: 'hfLoading'; data: LoadedData; input: string }
|
|
104
|
-
| { kind: 'hfFilePick'; data: LoadedData; input: string; repo: HuggingFaceRepoInfo; files: HuggingFaceSibling[] }
|
|
105
|
-
| { kind: 'hfReview'; data: LoadedData; plan: HfDownloadPlan }
|
|
106
|
-
| { kind: 'hfDownloading'; data: LoadedData; plan: HfDownloadPlan; progress: HfDownloadProgress }
|
|
107
|
-
| { kind: 'hfDone'; data: LoadedData; model: LocalHfModel; alreadyInstalled?: boolean }
|
|
108
|
-
| { kind: 'hfError'; data: LoadedData; message: string; input?: string }
|
|
109
|
-
| { kind: 'localUninstallPick'; data: LoadedData }
|
|
110
|
-
| { kind: 'localUninstallConfirm'; data: LoadedData; target: LocalUninstallTarget }
|
|
111
|
-
| { kind: 'localUninstalling'; data: LoadedData; target: LocalUninstallTarget }
|
|
112
|
-
| { kind: 'localUninstallDone'; data: LoadedData; modelName: string }
|
|
113
|
-
| { kind: 'localUninstallError'; data: LoadedData; target: LocalUninstallTarget; message: string }
|
|
114
|
-
| { kind: 'localRunnerSetup'; data: LoadedData; model: LocalHfModel }
|
|
115
|
-
| { kind: 'localRunnerInstalling'; data: LoadedData; model: LocalHfModel; startedAt: number; progress: LlamaCppInstallProgress }
|
|
116
|
-
| { kind: 'localRunnerInstallFail'; data: LoadedData; model: LocalHfModel; result: Extract<LlamaCppInstallResult, { ok: false }> }
|
|
117
|
-
| { kind: 'localRunnerPathEntry'; data: LoadedData; model: LocalHfModel; submitting: boolean; error?: string }
|
|
118
|
-
| { kind: 'localRunnerStarting'; data: LoadedData; model: LocalHfModel; startedAt: number }
|
|
119
|
-
| { kind: 'localRunnerStartFail'; data: LoadedData; model: LocalHfModel; result: Extract<LlamaCppStartResult, { ok: false }> }
|
|
120
|
-
| { kind: 'mmprojOffer'; data: LoadedData; model: LocalHfModel }
|
|
121
|
-
| { kind: 'mmprojDownloading'; data: LoadedData; model: LocalHfModel; progress: HfDownloadProgress }
|
|
122
|
-
| { kind: 'mmprojError'; data: LoadedData; model: LocalHfModel; message: string }
|
|
125
|
+
export type { ModelPickerSelection } from './modelPickerTypes.js'
|
|
126
|
+
export { chooseInstalledHfModelForRepo } from './modelPickerHfFlow.js'
|
|
123
127
|
|
|
124
128
|
export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
125
129
|
currentConfig,
|
|
@@ -1002,828 +1006,4 @@ function handleSubmit(
|
|
|
1002
1006
|
}
|
|
1003
1007
|
}
|
|
1004
1008
|
|
|
1005
|
-
|
|
1006
|
-
data: LoadedData,
|
|
1007
|
-
setState: (s: State) => void,
|
|
1008
|
-
): Promise<void> {
|
|
1009
|
-
setState({ kind: 'localCatalogLoading', data })
|
|
1010
|
-
try {
|
|
1011
|
-
const installedModels = await loadLocalHfModels()
|
|
1012
|
-
const catalog = await fetchUncensoredGgufCatalog({
|
|
1013
|
-
machineSpec: data.machineSpec,
|
|
1014
|
-
installedModels,
|
|
1015
|
-
})
|
|
1016
|
-
setState({
|
|
1017
|
-
kind: 'localCatalog',
|
|
1018
|
-
data: { ...data, hfModels: await loadHfPickerModels() },
|
|
1019
|
-
catalog,
|
|
1020
|
-
})
|
|
1021
|
-
} catch (err: unknown) {
|
|
1022
|
-
setState({ kind: 'localCatalogError', data, message: (err as Error).message })
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
async function reviewCatalogModel(
|
|
1027
|
-
state: Extract<State, { kind: 'localCatalog' }>,
|
|
1028
|
-
entry: UncensoredCatalogEntry,
|
|
1029
|
-
setState: (s: State) => void,
|
|
1030
|
-
): Promise<void> {
|
|
1031
|
-
const files = ggufFiles(entry.repo)
|
|
1032
|
-
const installed = chooseInstalledHfModelForRepo(
|
|
1033
|
-
await loadLocalHfModels(),
|
|
1034
|
-
entry.repo,
|
|
1035
|
-
files,
|
|
1036
|
-
entry.file.filename,
|
|
1037
|
-
state.data.machineSpec,
|
|
1038
|
-
)
|
|
1039
|
-
if (installed) {
|
|
1040
|
-
setState({
|
|
1041
|
-
kind: 'hfDone',
|
|
1042
|
-
data: { ...state.data, hfModels: await loadHfPickerModels() },
|
|
1043
|
-
model: installed,
|
|
1044
|
-
alreadyInstalled: true,
|
|
1045
|
-
})
|
|
1046
|
-
return
|
|
1047
|
-
}
|
|
1048
|
-
try {
|
|
1049
|
-
const plan = await createHfDownloadPlan(entry.repo.repoId, entry.file.filename)
|
|
1050
|
-
setState({ kind: 'hfReview', data: state.data, plan })
|
|
1051
|
-
} catch (err: unknown) {
|
|
1052
|
-
setState({ kind: 'hfError', data: state.data, message: (err as Error).message, input: entry.repo.repoId })
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
function buildCatalogOptions(
|
|
1057
|
-
provider: CloudProviderId,
|
|
1058
|
-
catalog: ModelCatalogResult | undefined,
|
|
1059
|
-
currentProvider: ProviderId,
|
|
1060
|
-
currentModel: string,
|
|
1061
|
-
contextFit?: ModelPickerContextFit | null,
|
|
1062
|
-
): SelectOption<string>[] {
|
|
1063
|
-
if (!catalog || catalog.entries.length === 0) {
|
|
1064
|
-
return [{
|
|
1065
|
-
value: `hdr:catalog-empty:${provider}`,
|
|
1066
|
-
label: 'No Models Found',
|
|
1067
|
-
disabled: true,
|
|
1068
|
-
role: 'notice',
|
|
1069
|
-
prefix: 'note',
|
|
1070
|
-
}]
|
|
1071
|
-
}
|
|
1072
|
-
const sourceById = new Map(catalog.entries.map(entry => [entry.id, entry.source]))
|
|
1073
|
-
return orderModelsForContextFit(provider, catalog.entries.map(entry => entry.id), contextFit).map(id => {
|
|
1074
|
-
const active = currentProvider === provider && currentModel === id
|
|
1075
|
-
const suffix = sourceById.get(id) === 'fallback' ? ' fallback' : ''
|
|
1076
|
-
const displayName = formatModelDisplayName(provider, id, { maxLength: 64 })
|
|
1077
|
-
return {
|
|
1078
|
-
value: `full:${provider}:${id}`,
|
|
1079
|
-
label: contextFitLabel(provider, id, `${displayName}${active ? ' *' : ''}${suffix}`, contextFit),
|
|
1080
|
-
role: 'option',
|
|
1081
|
-
}
|
|
1082
|
-
})
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
function parseCloudValue(value: string): { provider: CloudProviderId; model: string } | null {
|
|
1086
|
-
if (!value.startsWith('c:')) return null
|
|
1087
|
-
const rest = value.slice(2)
|
|
1088
|
-
const sep = rest.indexOf(':')
|
|
1089
|
-
if (sep === -1) return null
|
|
1090
|
-
const provider = rest.slice(0, sep)
|
|
1091
|
-
const model = rest.slice(sep + 1)
|
|
1092
|
-
if (!isCloudProvider(provider) || !model) return null
|
|
1093
|
-
return { provider, model }
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
function localModelOptionIndex(
|
|
1097
|
-
options: SelectOption<string>[],
|
|
1098
|
-
currentProvider: ProviderId,
|
|
1099
|
-
currentModel: string,
|
|
1100
|
-
): number {
|
|
1101
|
-
return options.findIndex(opt => {
|
|
1102
|
-
if (opt.disabled) return false
|
|
1103
|
-
if (opt.value.startsWith('hf:')) return opt.value.slice(3) === currentModel && currentProvider === 'llamacpp'
|
|
1104
|
-
if (opt.value.startsWith('uc:')) return opt.value.slice(3) === currentModel && currentProvider === 'llamacpp'
|
|
1105
|
-
return false
|
|
1106
|
-
})
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
function localOrCloudOptionIndex(
|
|
1110
|
-
options: SelectOption<string>[],
|
|
1111
|
-
currentProvider: ProviderId,
|
|
1112
|
-
currentModel: string,
|
|
1113
|
-
): number {
|
|
1114
|
-
return options.findIndex(opt => {
|
|
1115
|
-
if (opt.disabled) return false
|
|
1116
|
-
if (opt.value.startsWith('hf:')) return opt.value.slice(3) === currentModel && currentProvider === 'llamacpp'
|
|
1117
|
-
if (opt.value.startsWith('uc:')) return opt.value.slice(3) === currentModel && currentProvider === 'llamacpp'
|
|
1118
|
-
const cloud = parseCloudValue(opt.value)
|
|
1119
|
-
return cloud?.provider === currentProvider && cloud.model === currentModel
|
|
1120
|
-
})
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
function parseFullCatalogValue(value: string): { provider: CloudProviderId; model: string } | null {
|
|
1124
|
-
if (!value.startsWith('full:')) return null
|
|
1125
|
-
const rest = value.slice(5)
|
|
1126
|
-
const sep = rest.indexOf(':')
|
|
1127
|
-
if (sep === -1) return null
|
|
1128
|
-
const provider = rest.slice(0, sep)
|
|
1129
|
-
const model = rest.slice(sep + 1)
|
|
1130
|
-
if (!isCloudProvider(provider) || !model) return null
|
|
1131
|
-
return { provider, model }
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
function parseKeyValue(value: string): { action: 'set' | 'edit' | 'manage'; provider: CloudProviderId } | null {
|
|
1135
|
-
if (!value.startsWith('key:')) return null
|
|
1136
|
-
const parts = value.split(':')
|
|
1137
|
-
if (parts.length !== 3) return null
|
|
1138
|
-
const action = parts[1]
|
|
1139
|
-
const provider = parts[2]
|
|
1140
|
-
if (action !== 'set' && action !== 'edit' && action !== 'manage') return null
|
|
1141
|
-
if (!isCloudProvider(provider)) return null
|
|
1142
|
-
return { action, provider }
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
async function submitKey(
|
|
1146
|
-
state: Extract<State, { kind: 'keyEntry' }>,
|
|
1147
|
-
value: string,
|
|
1148
|
-
currentConfig: EthagentConfig,
|
|
1149
|
-
setState: (s: State) => void,
|
|
1150
|
-
): Promise<void> {
|
|
1151
|
-
const trimmed = value.trim()
|
|
1152
|
-
if (!trimmed) {
|
|
1153
|
-
setState({ ...state, error: 'key cannot be empty' })
|
|
1154
|
-
return
|
|
1155
|
-
}
|
|
1156
|
-
setState({ ...state, submitting: true, error: undefined })
|
|
1157
|
-
try {
|
|
1158
|
-
await setKey(state.provider, trimmed)
|
|
1159
|
-
const data = await refreshProviderKeyState(state.data, currentConfig, state.provider)
|
|
1160
|
-
setState({ kind: 'list', data })
|
|
1161
|
-
} catch (err: unknown) {
|
|
1162
|
-
setState({ ...state, submitting: false, error: (err as Error).message })
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
async function startOpenAIOAuthFlow(
|
|
1167
|
-
data: LoadedData,
|
|
1168
|
-
currentConfig: EthagentConfig,
|
|
1169
|
-
setState: (s: State) => void,
|
|
1170
|
-
serviceRef: React.MutableRefObject<OpenAIOAuthService | null>,
|
|
1171
|
-
onPick: (sel: ModelPickerSelection) => void,
|
|
1172
|
-
): Promise<void> {
|
|
1173
|
-
serviceRef.current?.cleanup()
|
|
1174
|
-
const service = new OpenAIOAuthService()
|
|
1175
|
-
serviceRef.current = service
|
|
1176
|
-
setState({ kind: 'oauthLogin', data, phase: 'waiting' })
|
|
1177
|
-
try {
|
|
1178
|
-
const result = await service.start(authUrl => {
|
|
1179
|
-
openExternalUrl(authUrl)
|
|
1180
|
-
setState({ kind: 'oauthLogin', data, phase: 'waiting', url: authUrl })
|
|
1181
|
-
})
|
|
1182
|
-
if (serviceRef.current !== service) return
|
|
1183
|
-
setState({ kind: 'oauthLogin', data, phase: 'exchanging' })
|
|
1184
|
-
if (result.kind === 'apikey') {
|
|
1185
|
-
if (typeof result.apiKey !== 'string' || result.apiKey.length === 0) {
|
|
1186
|
-
throw new Error(`OAuth result was apikey kind but apiKey is ${typeof result.apiKey}; refusing to store.`)
|
|
1187
|
-
}
|
|
1188
|
-
try {
|
|
1189
|
-
await setKey('openai', result.apiKey)
|
|
1190
|
-
} catch (err) {
|
|
1191
|
-
throw new Error(`Storing the OpenAI API key failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
let refreshed: LoadedData
|
|
1195
|
-
try {
|
|
1196
|
-
refreshed = await refreshProviderKeyState(data, currentConfig, 'openai')
|
|
1197
|
-
} catch (err) {
|
|
1198
|
-
throw new Error(`Refreshing the OpenAI provider state failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
1199
|
-
}
|
|
1200
|
-
if (serviceRef.current !== service) return
|
|
1201
|
-
serviceRef.current = null
|
|
1202
|
-
if (result.kind === 'oauth-only' && !isOpenAIOAuthAllowedModel(currentConfig.model)) {
|
|
1203
|
-
onPick({ kind: 'cloud', provider: 'openai', model: OPENAI_OAUTH_DEFAULT_MODEL, keyJustSet: true })
|
|
1204
|
-
return
|
|
1205
|
-
}
|
|
1206
|
-
setState({ kind: 'list', data: refreshed })
|
|
1207
|
-
} catch (err: unknown) {
|
|
1208
|
-
if (serviceRef.current !== service) return
|
|
1209
|
-
serviceRef.current = null
|
|
1210
|
-
const message = err instanceof Error ? err.message : String(err)
|
|
1211
|
-
if (message === 'OpenAI sign-in was cancelled.') {
|
|
1212
|
-
setState({ kind: 'list', data })
|
|
1213
|
-
return
|
|
1214
|
-
}
|
|
1215
|
-
setState({ kind: 'oauthLogin', data, phase: 'error', message })
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
async function deleteKey(
|
|
1220
|
-
state: Extract<State, { kind: 'keyManage' }>,
|
|
1221
|
-
currentConfig: EthagentConfig,
|
|
1222
|
-
setState: (s: State) => void,
|
|
1223
|
-
onPick: (sel: ModelPickerSelection) => void,
|
|
1224
|
-
currentProvider: ProviderId,
|
|
1225
|
-
): Promise<void> {
|
|
1226
|
-
setState({ ...state, submitting: true, error: undefined })
|
|
1227
|
-
try {
|
|
1228
|
-
await rmKey(state.provider)
|
|
1229
|
-
if (state.provider === 'openai') await rmOpenAIOAuthCredentials()
|
|
1230
|
-
const data = await refreshProviderKeyState(state.data, currentConfig, state.provider)
|
|
1231
|
-
if (currentProvider === state.provider) {
|
|
1232
|
-
const fallback = pickFallbackSelection(data, state.provider)
|
|
1233
|
-
if (fallback) {
|
|
1234
|
-
onPick(fallback)
|
|
1235
|
-
return
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
setState({ kind: 'list', data })
|
|
1239
|
-
} catch (err: unknown) {
|
|
1240
|
-
setState({ ...state, submitting: false, error: (err as Error).message })
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
async function signOutOAuth(
|
|
1245
|
-
state: Extract<State, { kind: 'oauthManage' }>,
|
|
1246
|
-
currentConfig: EthagentConfig,
|
|
1247
|
-
setState: (s: State) => void,
|
|
1248
|
-
onPick: (sel: ModelPickerSelection) => void,
|
|
1249
|
-
currentProvider: ProviderId,
|
|
1250
|
-
): Promise<void> {
|
|
1251
|
-
setState({ ...state, submitting: true, error: undefined })
|
|
1252
|
-
try {
|
|
1253
|
-
await rmKey('openai')
|
|
1254
|
-
await rmOpenAIOAuthCredentials()
|
|
1255
|
-
const data = await refreshProviderKeyState(state.data, currentConfig, 'openai')
|
|
1256
|
-
if (currentProvider === 'openai') {
|
|
1257
|
-
const fallback = pickFallbackSelection(data, 'openai')
|
|
1258
|
-
if (fallback) {
|
|
1259
|
-
onPick(fallback)
|
|
1260
|
-
return
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
setState({ kind: 'list', data })
|
|
1264
|
-
} catch (err: unknown) {
|
|
1265
|
-
setState({ ...state, submitting: false, error: (err as Error).message })
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
function pickFallbackSelection(data: LoadedData, removed: ProviderId): ModelPickerSelection | null {
|
|
1270
|
-
for (const provider of MODEL_PICKER_CLOUD_PROVIDERS) {
|
|
1271
|
-
if (provider === removed) continue
|
|
1272
|
-
if (data.cloudKeys[provider] !== true) continue
|
|
1273
|
-
const catalogModel = data.cloudCatalogs[provider]?.entries[0]?.id
|
|
1274
|
-
const model = catalogModel ?? defaultModelFor(provider)
|
|
1275
|
-
return { kind: 'cloud', provider, model, keyJustSet: false }
|
|
1276
|
-
}
|
|
1277
|
-
if (data.hfModels.length > 0) {
|
|
1278
|
-
return { kind: 'llamacpp', model: data.hfModels[0]!.id }
|
|
1279
|
-
}
|
|
1280
|
-
return null
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
async function refreshProviderKeyState(
|
|
1284
|
-
data: LoadedData,
|
|
1285
|
-
currentConfig: EthagentConfig,
|
|
1286
|
-
provider: CloudProviderId,
|
|
1287
|
-
): Promise<LoadedData> {
|
|
1288
|
-
clearModelCatalogCache()
|
|
1289
|
-
const apiKeySet = await hasKey(provider)
|
|
1290
|
-
const oauthSet = provider === 'openai' ? await hasOpenAIOAuthCredentials() : false
|
|
1291
|
-
const keySet = apiKeySet || oauthSet
|
|
1292
|
-
const cloudKeys = { ...data.cloudKeys, [provider]: keySet }
|
|
1293
|
-
const cloudCredentialKinds: Partial<Record<ProviderId, CloudCredentialKind>> = { ...(data.cloudCredentialKinds ?? {}) }
|
|
1294
|
-
if (oauthSet) cloudCredentialKinds[provider] = 'oauth'
|
|
1295
|
-
else if (apiKeySet) cloudCredentialKinds[provider] = 'apikey'
|
|
1296
|
-
else delete cloudCredentialKinds[provider]
|
|
1297
|
-
const cloudCatalogs = { ...data.cloudCatalogs }
|
|
1298
|
-
if (keySet) {
|
|
1299
|
-
cloudCatalogs[provider] = await discoverProviderModels(configForProvider(currentConfig, provider))
|
|
1300
|
-
} else {
|
|
1301
|
-
delete cloudCatalogs[provider]
|
|
1302
|
-
}
|
|
1303
|
-
return { ...data, cloudKeys, cloudCatalogs, cloudCredentialKinds }
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
function configForProvider(config: EthagentConfig, provider: CloudProviderId): EthagentConfig {
|
|
1307
|
-
return {
|
|
1308
|
-
...config,
|
|
1309
|
-
provider,
|
|
1310
|
-
model: config.provider === provider ? config.model : defaultModelFor(provider),
|
|
1311
|
-
baseUrl: provider === 'openai' && config.provider === 'openai' ? config.baseUrl : undefined,
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
export function buildHfFileOptions(
|
|
1316
|
-
repo: HuggingFaceRepoInfo,
|
|
1317
|
-
files: HuggingFaceSibling[],
|
|
1318
|
-
spec: SpecSnapshot | undefined,
|
|
1319
|
-
installedModelIds: string[] = [],
|
|
1320
|
-
): SelectOption<string>[] {
|
|
1321
|
-
const ordered = spec
|
|
1322
|
-
? orderGgufFilesForSpec(repo, files, spec)
|
|
1323
|
-
: files.map(file => ({ file, fit: 'unknown' as GgufMachineFit, score: 0, budgetBytes: 0 }))
|
|
1324
|
-
const recommended = spec ? ordered[0]?.file.filename : undefined
|
|
1325
|
-
const installed = new Set(installedModelIds)
|
|
1326
|
-
return ordered.map(item => {
|
|
1327
|
-
const size = item.file.sizeBytes ? formatBytes(item.file.sizeBytes) : ''
|
|
1328
|
-
const indicators = [
|
|
1329
|
-
item.file.filename === recommended ? 'Recommended' : '',
|
|
1330
|
-
installed.has(localModelId(repo.repoId, item.file.filename)) ? 'Installed' : '',
|
|
1331
|
-
]
|
|
1332
|
-
return {
|
|
1333
|
-
value: item.file.filename,
|
|
1334
|
-
label: item.file.filename,
|
|
1335
|
-
subtext: modelMetadataSubtext(size, indicators),
|
|
1336
|
-
role: 'option' as const,
|
|
1337
|
-
}
|
|
1338
|
-
})
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
function buildRunnerRecoveryOptions(
|
|
1342
|
-
_result: Extract<LlamaCppInstallResult, { ok: false }>,
|
|
1343
|
-
): SelectOption<'stop-and-retry' | 'path' | 'back'>[] {
|
|
1344
|
-
return [
|
|
1345
|
-
{ value: 'stop-and-retry', label: 'Stop and Retry', hint: 'Stop background runners and try the install again' },
|
|
1346
|
-
{ value: 'path', label: 'Use Existing Runner Path' },
|
|
1347
|
-
{ value: 'back', label: 'Back To Picker' },
|
|
1348
|
-
]
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
function localRunnerStartFailureSubtitle(result: Extract<LlamaCppStartResult, { ok: false }>): string {
|
|
1352
|
-
switch (result.code) {
|
|
1353
|
-
case 'readiness-timeout':
|
|
1354
|
-
return 'the local runner is still loading or did not answer in time'
|
|
1355
|
-
case 'runner-exited':
|
|
1356
|
-
return 'the local runner closed before becoming ready'
|
|
1357
|
-
case 'spawn-failed':
|
|
1358
|
-
return 'the local runner could not be started'
|
|
1359
|
-
case 'different-model-running':
|
|
1360
|
-
return result.message
|
|
1361
|
-
case 'model-file-missing':
|
|
1362
|
-
return result.message
|
|
1363
|
-
case 'runner-not-installed':
|
|
1364
|
-
return 'this machine still needs a local runner'
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
async function findInstalledHfModelForInput(input: string): Promise<LocalHfModel | null> {
|
|
1369
|
-
const ref = parseHuggingFaceRef(input)
|
|
1370
|
-
const installed = await loadLocalHfModels()
|
|
1371
|
-
return installed.find(model =>
|
|
1372
|
-
model.status === 'ready'
|
|
1373
|
-
&& model.repoId === ref.repoId
|
|
1374
|
-
&& (!ref.filename || model.filename === ref.filename)
|
|
1375
|
-
) ?? null
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
export function chooseInstalledHfModelForRepo(
|
|
1379
|
-
installed: LocalHfModel[],
|
|
1380
|
-
repo: HuggingFaceRepoInfo,
|
|
1381
|
-
files: HuggingFaceSibling[],
|
|
1382
|
-
requestedFilename: string | undefined,
|
|
1383
|
-
spec: SpecSnapshot | undefined,
|
|
1384
|
-
): LocalHfModel | null {
|
|
1385
|
-
const compatibleFiles = new Set(files.map(file => file.filename))
|
|
1386
|
-
const candidates = installed.filter(model =>
|
|
1387
|
-
model.status === 'ready'
|
|
1388
|
-
&& model.repoId === repo.repoId
|
|
1389
|
-
&& compatibleFiles.has(model.filename)
|
|
1390
|
-
&& (!requestedFilename || model.filename === requestedFilename)
|
|
1391
|
-
)
|
|
1392
|
-
if (requestedFilename || candidates.length <= 1) return candidates[0] ?? null
|
|
1393
|
-
|
|
1394
|
-
const orderedFiles = spec
|
|
1395
|
-
? orderGgufFilesForSpec(repo, files, spec).map(item => item.file.filename)
|
|
1396
|
-
: files.map(file => file.filename)
|
|
1397
|
-
for (const filename of orderedFiles) {
|
|
1398
|
-
const match = candidates.find(model => model.filename === filename)
|
|
1399
|
-
if (match) return match
|
|
1400
|
-
}
|
|
1401
|
-
return candidates[0] ?? null
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
async function inspectHfInput(
|
|
1405
|
-
state: Extract<State, { kind: 'hfInput' }>,
|
|
1406
|
-
value: string,
|
|
1407
|
-
setState: (s: State) => void,
|
|
1408
|
-
): Promise<void> {
|
|
1409
|
-
const input = value.trim()
|
|
1410
|
-
if (!input) {
|
|
1411
|
-
setState({ ...state, error: 'paste a model link or repo id' })
|
|
1412
|
-
return
|
|
1413
|
-
}
|
|
1414
|
-
setState({ kind: 'hfLoading', data: state.data, input })
|
|
1415
|
-
try {
|
|
1416
|
-
const ref = parseHuggingFaceRef(input)
|
|
1417
|
-
const repo = await fetchHuggingFaceRepoInfo(ref)
|
|
1418
|
-
const files = ggufFiles(repo)
|
|
1419
|
-
if (files.length === 0) {
|
|
1420
|
-
setState({
|
|
1421
|
-
kind: 'hfInput',
|
|
1422
|
-
data: state.data,
|
|
1423
|
-
error: 'no compatible local model files found; paste a different model link',
|
|
1424
|
-
})
|
|
1425
|
-
return
|
|
1426
|
-
}
|
|
1427
|
-
const installed = chooseInstalledHfModelForRepo(
|
|
1428
|
-
await loadLocalHfModels(),
|
|
1429
|
-
repo,
|
|
1430
|
-
files,
|
|
1431
|
-
ref.filename,
|
|
1432
|
-
state.data.machineSpec,
|
|
1433
|
-
)
|
|
1434
|
-
if (installed) {
|
|
1435
|
-
setState({
|
|
1436
|
-
kind: 'hfDone',
|
|
1437
|
-
data: { ...state.data, hfModels: await loadHfPickerModels() },
|
|
1438
|
-
model: installed,
|
|
1439
|
-
alreadyInstalled: true,
|
|
1440
|
-
})
|
|
1441
|
-
return
|
|
1442
|
-
}
|
|
1443
|
-
const recommendedFilename = state.data.machineSpec
|
|
1444
|
-
? recommendGgufFile(repo, files, state.data.machineSpec)?.file.filename
|
|
1445
|
-
: files[0]?.filename
|
|
1446
|
-
if (ref.filename || files.length === 1) {
|
|
1447
|
-
const plan = await createHfDownloadPlan(input, ref.filename ?? recommendedFilename)
|
|
1448
|
-
setState({ kind: 'hfReview', data: state.data, plan })
|
|
1449
|
-
return
|
|
1450
|
-
}
|
|
1451
|
-
setState({ kind: 'hfFilePick', data: state.data, input, repo, files })
|
|
1452
|
-
} catch (err: unknown) {
|
|
1453
|
-
setState({ kind: 'hfInput', data: state.data, error: (err as Error).message })
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
async function reviewHfFile(
|
|
1458
|
-
state: Extract<State, { kind: 'hfFilePick' }>,
|
|
1459
|
-
filename: string,
|
|
1460
|
-
setState: (s: State) => void,
|
|
1461
|
-
): Promise<void> {
|
|
1462
|
-
setState({ kind: 'hfLoading', data: state.data, input: state.input })
|
|
1463
|
-
try {
|
|
1464
|
-
const installed = chooseInstalledHfModelForRepo(
|
|
1465
|
-
await loadLocalHfModels(),
|
|
1466
|
-
state.repo,
|
|
1467
|
-
state.files,
|
|
1468
|
-
filename,
|
|
1469
|
-
state.data.machineSpec,
|
|
1470
|
-
)
|
|
1471
|
-
if (installed) {
|
|
1472
|
-
setState({
|
|
1473
|
-
kind: 'hfDone',
|
|
1474
|
-
data: { ...state.data, hfModels: await loadHfPickerModels() },
|
|
1475
|
-
model: installed,
|
|
1476
|
-
alreadyInstalled: true,
|
|
1477
|
-
})
|
|
1478
|
-
return
|
|
1479
|
-
}
|
|
1480
|
-
const plan = await createHfDownloadPlan(state.input, filename)
|
|
1481
|
-
setState({ kind: 'hfReview', data: state.data, plan })
|
|
1482
|
-
} catch (err: unknown) {
|
|
1483
|
-
setState({ kind: 'hfError', data: state.data, message: (err as Error).message, input: state.input })
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
async function startHfDownload(
|
|
1488
|
-
state: Extract<State, { kind: 'hfReview' }>,
|
|
1489
|
-
setState: (s: State) => void,
|
|
1490
|
-
abortRef: React.MutableRefObject<AbortController | null>,
|
|
1491
|
-
onPick: (sel: ModelPickerSelection) => void,
|
|
1492
|
-
): Promise<void> {
|
|
1493
|
-
const controller = new AbortController()
|
|
1494
|
-
abortRef.current = controller
|
|
1495
|
-
setState({ kind: 'hfDownloading', data: state.data, plan: state.plan, progress: { status: 'starting', completed: 0, total: state.plan.sizeBytes } })
|
|
1496
|
-
try {
|
|
1497
|
-
for await (const progress of downloadHfModel(state.plan, controller.signal)) {
|
|
1498
|
-
if (controller.signal.aborted) return
|
|
1499
|
-
setState({ kind: 'hfDownloading', data: state.data, plan: state.plan, progress })
|
|
1500
|
-
}
|
|
1501
|
-
const model = await findLocalHfModel(`${state.plan.repoId}#${state.plan.filename}`)
|
|
1502
|
-
?? modelFromPlan(state.plan, undefined, 'ready')
|
|
1503
|
-
const data = {
|
|
1504
|
-
...state.data,
|
|
1505
|
-
hfModels: await loadHfPickerModels(),
|
|
1506
|
-
}
|
|
1507
|
-
await startAndPickHfModel(model, { kind: 'hfDone', data, model }, setState, onPick)
|
|
1508
|
-
} catch (err: unknown) {
|
|
1509
|
-
if (controller.signal.aborted) return
|
|
1510
|
-
setState({ kind: 'hfError', data: state.data, message: (err as Error).message, input: state.plan.repoId })
|
|
1511
|
-
} finally {
|
|
1512
|
-
abortRef.current = null
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
function localUninstallTargets(data: LoadedData): LocalUninstallTarget[] {
|
|
1517
|
-
return data.hfModels.map(model => ({
|
|
1518
|
-
kind: 'hf' as const,
|
|
1519
|
-
id: model.id,
|
|
1520
|
-
displayName: formatLocalHfModelDisplayName(model.id, {
|
|
1521
|
-
displayName: model.displayName,
|
|
1522
|
-
maxLength: 64,
|
|
1523
|
-
}),
|
|
1524
|
-
sizeBytes: model.sizeBytes,
|
|
1525
|
-
}))
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
function isCurrentLocalUninstallTarget(
|
|
1529
|
-
target: LocalUninstallTarget,
|
|
1530
|
-
currentProvider: ProviderId,
|
|
1531
|
-
currentModel: string,
|
|
1532
|
-
): boolean {
|
|
1533
|
-
return target.kind === 'hf' && currentProvider === 'llamacpp' && target.id === currentModel
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
function localUninstallBoundaryCopy(_target: LocalUninstallTarget): string {
|
|
1537
|
-
return 'This removes only the downloaded GGUF file and metadata from this machine.'
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
async function uninstallLocalModel(
|
|
1541
|
-
state: Extract<State, { kind: 'localUninstallConfirm' }>,
|
|
1542
|
-
setState: (s: State) => void,
|
|
1543
|
-
): Promise<void> {
|
|
1544
|
-
setState({ kind: 'localUninstalling', data: state.data, target: state.target })
|
|
1545
|
-
const modelName = state.target.displayName
|
|
1546
|
-
try {
|
|
1547
|
-
await uninstallLocalHfModel(state.target.id)
|
|
1548
|
-
const data = await refreshLocalModelData(state.data)
|
|
1549
|
-
setState({ kind: 'localUninstallDone', data, modelName })
|
|
1550
|
-
} catch (err: unknown) {
|
|
1551
|
-
setState({
|
|
1552
|
-
kind: 'localUninstallError',
|
|
1553
|
-
data: state.data,
|
|
1554
|
-
target: state.target,
|
|
1555
|
-
message: (err as Error).message,
|
|
1556
|
-
})
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
async function downloadMmprojAndContinue(
|
|
1561
|
-
state: Extract<State, { kind: 'mmprojOffer' }>,
|
|
1562
|
-
setState: (s: State) => void,
|
|
1563
|
-
onPick: (sel: ModelPickerSelection) => void,
|
|
1564
|
-
): Promise<void> {
|
|
1565
|
-
setState({ kind: 'mmprojDownloading', data: state.data, model: state.model, progress: { status: 'starting' } })
|
|
1566
|
-
try {
|
|
1567
|
-
for await (const progress of addMmprojToInstalledModel(state.model.id)) {
|
|
1568
|
-
setState({ kind: 'mmprojDownloading', data: state.data, model: state.model, progress })
|
|
1569
|
-
}
|
|
1570
|
-
} catch (err: unknown) {
|
|
1571
|
-
setState({ kind: 'mmprojError', data: state.data, model: state.model, message: (err as Error).message })
|
|
1572
|
-
return
|
|
1573
|
-
}
|
|
1574
|
-
const updated = await findLocalHfModel(state.model.id)
|
|
1575
|
-
if (!updated || !updated.mmprojPath) {
|
|
1576
|
-
setState({ kind: 'mmprojError', data: state.data, model: state.model, message: 'projector downloaded but path was not persisted' })
|
|
1577
|
-
return
|
|
1578
|
-
}
|
|
1579
|
-
await stopLlamaCppServer().catch(() => null)
|
|
1580
|
-
const data = { ...state.data, hfModels: await loadHfPickerModels() }
|
|
1581
|
-
await startAndPickHfModel(updated, { kind: 'mmprojOffer', data, model: updated }, setState, onPick)
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
async function refreshLocalModelData(data: LoadedData): Promise<LoadedData> {
|
|
1585
|
-
const hfModels = await loadHfPickerModels()
|
|
1586
|
-
return {
|
|
1587
|
-
...data,
|
|
1588
|
-
hfModels,
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
async function startAndPickHfModel(
|
|
1593
|
-
model: LocalHfModel,
|
|
1594
|
-
state: Extract<State, { kind: 'list' | 'localCatalog' | 'hfDone' | 'mmprojOffer' | 'mmprojError' }>,
|
|
1595
|
-
setState: (s: State) => void,
|
|
1596
|
-
onPick: (sel: ModelPickerSelection) => void,
|
|
1597
|
-
): Promise<void> {
|
|
1598
|
-
if (model.risk === 'high') {
|
|
1599
|
-
setState({ kind: 'hfError', data: state.data, message: 'blocked high-risk model; choose a model from a more credible source' })
|
|
1600
|
-
return
|
|
1601
|
-
}
|
|
1602
|
-
if (model.mmprojAvailable && !model.mmprojPath && state.kind !== 'mmprojOffer' && state.kind !== 'mmprojError') {
|
|
1603
|
-
setState({ kind: 'mmprojOffer', data: state.data, model })
|
|
1604
|
-
return
|
|
1605
|
-
}
|
|
1606
|
-
setState({ kind: 'localRunnerStarting', data: state.data, model, startedAt: Date.now() })
|
|
1607
|
-
const result = await startLlamaCppServer({
|
|
1608
|
-
modelPath: model.localPath,
|
|
1609
|
-
modelAlias: model.id,
|
|
1610
|
-
mmprojPath: model.mmprojPath,
|
|
1611
|
-
})
|
|
1612
|
-
const llamaCpp = await probeLlamaCpp()
|
|
1613
|
-
const data = { ...state.data, llamaCpp }
|
|
1614
|
-
if (!result.ok) {
|
|
1615
|
-
if (result.code === 'runner-not-installed') {
|
|
1616
|
-
setState({ kind: 'localRunnerSetup', data, model })
|
|
1617
|
-
return
|
|
1618
|
-
}
|
|
1619
|
-
setState({ kind: 'localRunnerStartFail', data, model, result })
|
|
1620
|
-
return
|
|
1621
|
-
}
|
|
1622
|
-
onPick({ kind: 'llamacpp', model: model.id, mmprojPath: model.mmprojPath })
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
async function installRunnerAndStart(
|
|
1626
|
-
state: Extract<State, { kind: 'localRunnerSetup' }>,
|
|
1627
|
-
setState: (s: State) => void,
|
|
1628
|
-
onPick: (sel: ModelPickerSelection) => void,
|
|
1629
|
-
): Promise<void> {
|
|
1630
|
-
await runRunnerSetup(state, setState, onPick, installLlamaCppRunner)
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
async function runRunnerSetup(
|
|
1634
|
-
state: Extract<State, { kind: 'localRunnerSetup' }>,
|
|
1635
|
-
setState: (s: State) => void,
|
|
1636
|
-
onPick: (sel: ModelPickerSelection) => void,
|
|
1637
|
-
setup: (onProgress?: (progress: LlamaCppInstallProgress) => void) => Promise<LlamaCppInstallResult>,
|
|
1638
|
-
): Promise<void> {
|
|
1639
|
-
const startedAt = Date.now()
|
|
1640
|
-
const initialProgress: LlamaCppInstallProgress = {
|
|
1641
|
-
phase: 'checking',
|
|
1642
|
-
label: 'preparing local runner...',
|
|
1643
|
-
progress: 0.04,
|
|
1644
|
-
}
|
|
1645
|
-
const updateProgress = (progress: LlamaCppInstallProgress): void => {
|
|
1646
|
-
setState({ kind: 'localRunnerInstalling', data: state.data, model: state.model, startedAt, progress })
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
setState({ kind: 'localRunnerInstalling', data: state.data, model: state.model, startedAt, progress: initialProgress })
|
|
1650
|
-
const result = await setup(updateProgress)
|
|
1651
|
-
if (!result.ok) {
|
|
1652
|
-
setState({ kind: 'localRunnerInstallFail', data: state.data, model: state.model, result })
|
|
1653
|
-
return
|
|
1654
|
-
}
|
|
1655
|
-
await startAndPickHfModel(state.model, { kind: 'hfDone', data: state.data, model: state.model }, setState, onPick)
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
async function saveRunnerPathAndStart(
|
|
1659
|
-
state: Extract<State, { kind: 'localRunnerPathEntry' }>,
|
|
1660
|
-
value: string,
|
|
1661
|
-
setState: (s: State) => void,
|
|
1662
|
-
onPick: (sel: ModelPickerSelection) => void,
|
|
1663
|
-
): Promise<void> {
|
|
1664
|
-
const runnerPath = value.trim().replace(/^"|"$/g, '')
|
|
1665
|
-
if (!runnerPath) {
|
|
1666
|
-
setState({ ...state, error: 'paste the full path to llama-server' })
|
|
1667
|
-
return
|
|
1668
|
-
}
|
|
1669
|
-
setState({ ...state, submitting: true, error: undefined })
|
|
1670
|
-
try {
|
|
1671
|
-
await setLlamaCppServerPath(runnerPath)
|
|
1672
|
-
await startAndPickHfModel(state.model, { kind: 'hfDone', data: state.data, model: state.model }, setState, onPick)
|
|
1673
|
-
} catch (err: unknown) {
|
|
1674
|
-
setState({ ...state, submitting: false, error: (err as Error).message })
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
function contextFitSubtitle(contextFit: ModelPickerContextFit): string {
|
|
1679
|
-
const threshold = contextFit.thresholdPercent ?? 90
|
|
1680
|
-
return `pending prompt needs ~${formatTokens(contextFit.usedTokens)} tokens; choose a model under ${threshold}% or use /compact.`
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
function contextFitLabel(
|
|
1684
|
-
provider: ProviderId,
|
|
1685
|
-
model: string,
|
|
1686
|
-
baseLabel: string,
|
|
1687
|
-
contextFit?: ModelPickerContextFit | null,
|
|
1688
|
-
): string {
|
|
1689
|
-
if (!contextFit) return baseLabel
|
|
1690
|
-
const info = contextWindowInfo(provider, model)
|
|
1691
|
-
const percent = info.tokens > 0 ? Math.round((contextFit.usedTokens / info.tokens) * 100) : 0
|
|
1692
|
-
return `${baseLabel} ${formatContextWindow(info.tokens)} ctx ${percent}%`
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
function formatTokens(count: number): string {
|
|
1696
|
-
if (count < 1000) return String(count)
|
|
1697
|
-
if (count < 10_000) return `${(count / 1000).toFixed(1)}k`
|
|
1698
|
-
return `${Math.round(count / 1000)}k`
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
function formatContextWindow(tokens: number): string {
|
|
1702
|
-
if (tokens >= 1_000_000) {
|
|
1703
|
-
const millions = tokens / 1_000_000
|
|
1704
|
-
return Number.isInteger(millions) ? `${millions}m` : `${millions.toFixed(1)}m`
|
|
1705
|
-
}
|
|
1706
|
-
if (tokens >= 1000) return `${Math.round(tokens / 1000)}k`
|
|
1707
|
-
return String(tokens)
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
async function loadHfPickerModels(): Promise<ModelPickerOptionsData['hfModels']> {
|
|
1711
|
-
const installed = await loadLocalHfModels()
|
|
1712
|
-
const backfilled = await backfillMmprojForModels(installed)
|
|
1713
|
-
return backfilled.map(model => ({
|
|
1714
|
-
id: model.id,
|
|
1715
|
-
displayName: model.displayName,
|
|
1716
|
-
sizeBytes: model.sizeBytes,
|
|
1717
|
-
quantization: model.quantization,
|
|
1718
|
-
risk: model.risk,
|
|
1719
|
-
task: model.task,
|
|
1720
|
-
status: model.status,
|
|
1721
|
-
mmprojPath: model.mmprojPath,
|
|
1722
|
-
mmprojAvailable: model.mmprojAvailable,
|
|
1723
|
-
mmprojSizeBytes: model.mmprojSizeBytes,
|
|
1724
|
-
}))
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
async function probeLlamaCpp(): Promise<ModelPickerOptionsData['llamaCpp']> {
|
|
1728
|
-
try {
|
|
1729
|
-
const status = await detectLlamaCpp()
|
|
1730
|
-
return {
|
|
1731
|
-
binaryPresent: status.binaryPresent,
|
|
1732
|
-
serverUp: status.serverUp,
|
|
1733
|
-
}
|
|
1734
|
-
} catch (err: unknown) {
|
|
1735
|
-
return { binaryPresent: false, serverUp: false, error: (err as Error).message }
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
function formatBytes(bytes: number): string {
|
|
1740
|
-
if (bytes <= 0) return 'size unknown'
|
|
1741
|
-
const gb = bytes / 1e9
|
|
1742
|
-
if (gb >= 1) return `${gb.toFixed(1)} GB`
|
|
1743
|
-
return `${Math.round(bytes / 1e6)} MB`
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
function modelMetadataSubtext(size: string, indicators: string[]): string | undefined {
|
|
1747
|
-
return [size, ...indicators].filter(Boolean).join(' · ') || undefined
|
|
1748
|
-
}
|
|
1749
|
-
|
|
1750
|
-
function riskColor(risk: string): string {
|
|
1751
|
-
if (risk === 'high') return theme.accentError
|
|
1752
|
-
if (risk === 'medium') return theme.dim
|
|
1753
|
-
return theme.accentPeriwinkle
|
|
1754
|
-
}
|
|
1755
|
-
|
|
1756
|
-
function fitColor(fit: GgufMachineFit): string {
|
|
1757
|
-
if (fit === 'too-large') return theme.accentError
|
|
1758
|
-
if (fit === 'tight') return theme.accentPeriwinkle
|
|
1759
|
-
return theme.dim
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
function fitLabel(fit: GgufMachineFit, recommended: boolean): string {
|
|
1763
|
-
if (recommended && fit !== 'too-large') return 'Recommended for this machine'
|
|
1764
|
-
if (recommended) return 'Best match found; may be too large'
|
|
1765
|
-
return fileFitHint(fit)
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
function fileFitHint(fit: GgufMachineFit): string {
|
|
1769
|
-
switch (fit) {
|
|
1770
|
-
case 'fits': return 'Fits this machine'
|
|
1771
|
-
case 'tight': return 'May be slow or tight on memory'
|
|
1772
|
-
case 'too-large': return 'Likely too large for this machine'
|
|
1773
|
-
case 'unknown': return 'machine fit unknown'
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
function formatSignals(downloads: number | undefined, likes: number | undefined): string {
|
|
1778
|
-
const d = downloads == null ? 'downloads unknown' : `${downloads} downloads`
|
|
1779
|
-
const l = likes == null ? 'likes unknown' : `${likes} likes`
|
|
1780
|
-
return `${d}, ${l}`
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
function friendlyFileName(filename: string): string {
|
|
1784
|
-
return filename.split('/').pop() ?? filename
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
function safetyLabel(risk: HfRisk): string {
|
|
1788
|
-
if (risk === 'low') return 'reviewed'
|
|
1789
|
-
if (risk === 'medium') return 'needs review'
|
|
1790
|
-
return 'blocked'
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
function credibilityLabel(credibility: HfCredibility): string {
|
|
1794
|
-
if (credibility === 'established') return 'established'
|
|
1795
|
-
if (credibility === 'normal') return 'some signals'
|
|
1796
|
-
return 'limited signals'
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
function friendlyReasons(reasons: string[]): string[] {
|
|
1800
|
-
return reasons.map(reason => {
|
|
1801
|
-
if (reason.includes('compatible local model file')) return 'compatible local model file'
|
|
1802
|
-
if (reason.includes('selected file is not compatible')) return 'file is not compatible with local chat'
|
|
1803
|
-
if (reason.includes('revision is mutable')) return 'model link may point to changing files'
|
|
1804
|
-
if (reason.includes('license is missing')) return 'license is missing'
|
|
1805
|
-
if (reason.includes('limited public usage signals')) return 'source has limited public usage'
|
|
1806
|
-
if (reason.includes('pickle/bin')) return 'repo also contains risky model file formats'
|
|
1807
|
-
return reason
|
|
1808
|
-
})
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
function providerKeyPlaceholder(provider: ProviderId): string {
|
|
1812
|
-
if (provider === 'openai') return 'sk-...'
|
|
1813
|
-
if (provider === 'anthropic') return 'sk-ant-...'
|
|
1814
|
-
if (provider === 'gemini') return 'AIza...'
|
|
1815
|
-
return ''
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
function runnerPathPlaceholder(): string {
|
|
1819
|
-
if (process.platform === 'win32') return 'C:\\path\\to\\llama-server.exe'
|
|
1820
|
-
return '/path/to/llama-server'
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
function isCloudProvider(value: string | undefined): value is CloudProviderId {
|
|
1824
|
-
return value === 'openai' || value === 'anthropic' || value === 'gemini'
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
const ElapsedSpinner: React.FC<{ startedAt: number; label: string }> = ({ startedAt, label }) => {
|
|
1828
|
-
return <Spinner label={label} startedAt={startedAt} />
|
|
1829
|
-
}
|
|
1009
|
+
export { buildHfFileOptions } from './modelPickerViewHelpers.js'
|