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.
Files changed (73) hide show
  1. package/README.md +6 -1
  2. package/package.json +3 -1
  3. package/src/app/FirstRun.tsx +1 -24
  4. package/src/app/firstRunConfig.ts +26 -0
  5. package/src/auth/openaiOAuth/landingPage.ts +2 -11
  6. package/src/chat/ChatScreen.tsx +32 -117
  7. package/src/chat/MessageList.tsx +18 -260
  8. package/src/chat/chatEnvironment.ts +16 -0
  9. package/src/chat/chatTurnContext.ts +50 -0
  10. package/src/chat/chatTurnOrchestrator.ts +5 -112
  11. package/src/chat/chatTurnRows.ts +64 -0
  12. package/src/chat/commands.ts +3 -178
  13. package/src/chat/continuityEditReview.ts +42 -0
  14. package/src/chat/input/ChatInput.tsx +10 -144
  15. package/src/chat/input/chatInputHelpers.ts +62 -0
  16. package/src/chat/input/inputRendering.tsx +93 -0
  17. package/src/chat/messageMarkdown.ts +220 -0
  18. package/src/chat/messageRows.ts +43 -0
  19. package/src/chat/planImplementation.ts +62 -0
  20. package/src/chat/slashCommandHandlers.ts +165 -0
  21. package/src/chat/slashCommandViews.ts +120 -0
  22. package/src/cli/main.tsx +7 -0
  23. package/src/identity/continuity/challenges.ts +123 -0
  24. package/src/identity/continuity/envelope.ts +49 -1484
  25. package/src/identity/continuity/envelopeCreate.ts +322 -0
  26. package/src/identity/continuity/envelopeCrypto.ts +182 -0
  27. package/src/identity/continuity/envelopeParse.ts +441 -0
  28. package/src/identity/continuity/envelopeTypes.ts +204 -0
  29. package/src/identity/continuity/envelopeVersion.ts +1 -0
  30. package/src/identity/continuity/payloadNormalization.ts +183 -0
  31. package/src/identity/continuity/publicSkills.ts +5 -5
  32. package/src/identity/continuity/skills/loadSkills.ts +12 -69
  33. package/src/identity/continuity/skills/skillPaths.ts +76 -0
  34. package/src/identity/continuity/skillsNormalization.ts +119 -0
  35. package/src/identity/continuity/snapshotToken.ts +28 -0
  36. package/src/identity/hub/continuity/completion.ts +67 -0
  37. package/src/identity/hub/continuity/effects.ts +5 -62
  38. package/src/identity/hub/profile/effects.ts +6 -170
  39. package/src/identity/hub/profile/operatorSave.ts +202 -0
  40. package/src/identity/registry/erc8004/metadata.ts +31 -23
  41. package/src/identity/wallet/browserWallet/html.ts +1 -57
  42. package/src/identity/wallet/browserWallet/walletPageSource.ts +85 -0
  43. package/src/identity/wallet/page/controller.ts +1 -1
  44. package/src/identity/wallet/page/errorView.ts +122 -0
  45. package/src/identity/wallet/page/view.ts +3 -114
  46. package/src/mcp/manager.ts +8 -66
  47. package/src/mcp/managerHelpers.ts +70 -0
  48. package/src/models/ModelPicker.tsx +69 -889
  49. package/src/models/huggingface.ts +20 -137
  50. package/src/models/huggingfaceStorage.ts +136 -0
  51. package/src/models/llamacpp.ts +37 -303
  52. package/src/models/llamacppCommands.ts +44 -0
  53. package/src/models/llamacppConfig.ts +34 -0
  54. package/src/models/llamacppDiscovery.ts +176 -0
  55. package/src/models/llamacppOutput.ts +65 -0
  56. package/src/models/modelPickerCatalogFlow.ts +56 -0
  57. package/src/models/modelPickerCredentials.ts +166 -0
  58. package/src/models/modelPickerData.ts +41 -0
  59. package/src/models/modelPickerDisplay.tsx +132 -0
  60. package/src/models/modelPickerHfFlow.ts +192 -0
  61. package/src/models/modelPickerLocalRunnerFlow.ts +115 -0
  62. package/src/models/modelPickerTypes.ts +69 -0
  63. package/src/models/modelPickerUninstallFlow.ts +48 -0
  64. package/src/models/modelPickerViewHelpers.ts +174 -0
  65. package/src/providers/openai-chat.ts +5 -124
  66. package/src/providers/openaiChatWire.ts +124 -0
  67. package/src/runtime/providerTurn.ts +38 -0
  68. package/src/runtime/textToolParser.ts +161 -0
  69. package/src/runtime/toolIntent.ts +1 -1
  70. package/src/runtime/turn.ts +43 -499
  71. package/src/runtime/turnNudges.ts +223 -0
  72. package/src/runtime/turnTypes.ts +86 -0
  73. 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
- | { kind: 'llamacpp'; model: string; mmprojPath?: string }
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
- async function openLocalCatalog(
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'