ethagent 3.0.2 → 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 +15 -116
- 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/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/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/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
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LlamaCppInstallProgress,
|
|
3
|
+
LlamaCppInstallResult,
|
|
4
|
+
LlamaCppStartResult,
|
|
5
|
+
} from './llamacpp.js'
|
|
6
|
+
import {
|
|
7
|
+
installLlamaCppRunner,
|
|
8
|
+
setLlamaCppServerPath,
|
|
9
|
+
startLlamaCppServer,
|
|
10
|
+
} from './llamacpp.js'
|
|
11
|
+
import type { LocalHfModel } from './huggingface.js'
|
|
12
|
+
import type { ModelPickerSelection, ModelPickerState as State } from './modelPickerTypes.js'
|
|
13
|
+
import { loadHfPickerModels, probeLlamaCpp } from './modelPickerData.js'
|
|
14
|
+
export function localRunnerStartFailureSubtitle(result: Extract<LlamaCppStartResult, { ok: false }>): string {
|
|
15
|
+
switch (result.code) {
|
|
16
|
+
case 'readiness-timeout':
|
|
17
|
+
return 'the local runner is still loading or did not answer in time'
|
|
18
|
+
case 'runner-exited':
|
|
19
|
+
return 'the local runner closed before becoming ready'
|
|
20
|
+
case 'spawn-failed':
|
|
21
|
+
return 'the local runner could not be started'
|
|
22
|
+
case 'different-model-running':
|
|
23
|
+
return result.message
|
|
24
|
+
case 'model-file-missing':
|
|
25
|
+
return result.message
|
|
26
|
+
case 'runner-not-installed':
|
|
27
|
+
return 'this machine still needs a local runner'
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function startAndPickHfModel(
|
|
32
|
+
model: LocalHfModel,
|
|
33
|
+
state: Extract<State, { kind: 'list' | 'localCatalog' | 'hfDone' | 'mmprojOffer' | 'mmprojError' }>,
|
|
34
|
+
setState: (s: State) => void,
|
|
35
|
+
onPick: (sel: ModelPickerSelection) => void,
|
|
36
|
+
): Promise<void> {
|
|
37
|
+
if (model.risk === 'high') {
|
|
38
|
+
setState({ kind: 'hfError', data: state.data, message: 'blocked high-risk model; choose a model from a more credible source' })
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
if (model.mmprojAvailable && !model.mmprojPath && state.kind !== 'mmprojOffer' && state.kind !== 'mmprojError') {
|
|
42
|
+
setState({ kind: 'mmprojOffer', data: state.data, model })
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
setState({ kind: 'localRunnerStarting', data: state.data, model, startedAt: Date.now() })
|
|
46
|
+
const result = await startLlamaCppServer({
|
|
47
|
+
modelPath: model.localPath,
|
|
48
|
+
modelAlias: model.id,
|
|
49
|
+
mmprojPath: model.mmprojPath,
|
|
50
|
+
})
|
|
51
|
+
const llamaCpp = await probeLlamaCpp()
|
|
52
|
+
const data = { ...state.data, llamaCpp }
|
|
53
|
+
if (!result.ok) {
|
|
54
|
+
if (result.code === 'runner-not-installed') {
|
|
55
|
+
setState({ kind: 'localRunnerSetup', data, model })
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
setState({ kind: 'localRunnerStartFail', data, model, result })
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
onPick({ kind: 'llamacpp', model: model.id, mmprojPath: model.mmprojPath })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function installRunnerAndStart(
|
|
65
|
+
state: Extract<State, { kind: 'localRunnerSetup' }>,
|
|
66
|
+
setState: (s: State) => void,
|
|
67
|
+
onPick: (sel: ModelPickerSelection) => void,
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
await runRunnerSetup(state, setState, onPick, installLlamaCppRunner)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function runRunnerSetup(
|
|
73
|
+
state: Extract<State, { kind: 'localRunnerSetup' }>,
|
|
74
|
+
setState: (s: State) => void,
|
|
75
|
+
onPick: (sel: ModelPickerSelection) => void,
|
|
76
|
+
setup: (onProgress?: (progress: LlamaCppInstallProgress) => void) => Promise<LlamaCppInstallResult>,
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
const startedAt = Date.now()
|
|
79
|
+
const initialProgress: LlamaCppInstallProgress = {
|
|
80
|
+
phase: 'checking',
|
|
81
|
+
label: 'preparing local runner...',
|
|
82
|
+
progress: 0.04,
|
|
83
|
+
}
|
|
84
|
+
const updateProgress = (progress: LlamaCppInstallProgress): void => {
|
|
85
|
+
setState({ kind: 'localRunnerInstalling', data: state.data, model: state.model, startedAt, progress })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setState({ kind: 'localRunnerInstalling', data: state.data, model: state.model, startedAt, progress: initialProgress })
|
|
89
|
+
const result = await setup(updateProgress)
|
|
90
|
+
if (!result.ok) {
|
|
91
|
+
setState({ kind: 'localRunnerInstallFail', data: state.data, model: state.model, result })
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
await startAndPickHfModel(state.model, { kind: 'hfDone', data: state.data, model: state.model }, setState, onPick)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function saveRunnerPathAndStart(
|
|
98
|
+
state: Extract<State, { kind: 'localRunnerPathEntry' }>,
|
|
99
|
+
value: string,
|
|
100
|
+
setState: (s: State) => void,
|
|
101
|
+
onPick: (sel: ModelPickerSelection) => void,
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
const runnerPath = value.trim().replace(/^"|"$/g, '')
|
|
104
|
+
if (!runnerPath) {
|
|
105
|
+
setState({ ...state, error: 'paste the full path to llama-server' })
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
setState({ ...state, submitting: true, error: undefined })
|
|
109
|
+
try {
|
|
110
|
+
await setLlamaCppServerPath(runnerPath)
|
|
111
|
+
await startAndPickHfModel(state.model, { kind: 'hfDone', data: state.data, model: state.model }, setState, onPick)
|
|
112
|
+
} catch (err: unknown) {
|
|
113
|
+
setState({ ...state, submitting: false, error: (err as Error).message })
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { EthagentConfig, ProviderId } from '../storage/config.js'
|
|
2
|
+
import type {
|
|
3
|
+
LlamaCppInstallProgress,
|
|
4
|
+
LlamaCppInstallResult,
|
|
5
|
+
LlamaCppStartResult,
|
|
6
|
+
} from './llamacpp.js'
|
|
7
|
+
import type {
|
|
8
|
+
HfDownloadPlan,
|
|
9
|
+
HfDownloadProgress,
|
|
10
|
+
HuggingFaceRepoInfo,
|
|
11
|
+
HuggingFaceSibling,
|
|
12
|
+
LocalHfModel,
|
|
13
|
+
} from './huggingface.js'
|
|
14
|
+
import type {
|
|
15
|
+
CloudProviderId,
|
|
16
|
+
ModelPickerContextFit,
|
|
17
|
+
ModelPickerOptionsData,
|
|
18
|
+
} from './modelPickerOptions.js'
|
|
19
|
+
|
|
20
|
+
export type ModelPickerSelection =
|
|
21
|
+
| { kind: 'llamacpp'; model: string; mmprojPath?: string }
|
|
22
|
+
| { kind: 'cloud'; provider: CloudProviderId; model: string; keyJustSet: boolean }
|
|
23
|
+
|
|
24
|
+
export type ModelPickerProps = {
|
|
25
|
+
currentConfig: EthagentConfig
|
|
26
|
+
currentProvider: ProviderId
|
|
27
|
+
currentModel: string
|
|
28
|
+
contextFit?: ModelPickerContextFit | null
|
|
29
|
+
featuredHfRepo?: string
|
|
30
|
+
localOnly?: boolean
|
|
31
|
+
onPick: (selection: ModelPickerSelection) => void
|
|
32
|
+
onCancel: () => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type LoadedModelPickerData = ModelPickerOptionsData
|
|
36
|
+
export type LocalUninstallTarget = { kind: 'hf'; id: string; displayName: string; sizeBytes: number }
|
|
37
|
+
|
|
38
|
+
export type ModelPickerState =
|
|
39
|
+
| { kind: 'loading' }
|
|
40
|
+
| { kind: 'list'; data: LoadedModelPickerData }
|
|
41
|
+
| { kind: 'localCatalogLoading'; data: LoadedModelPickerData }
|
|
42
|
+
| { kind: 'localCatalog'; data: LoadedModelPickerData; catalog: import('./uncensoredCatalog.js').UncensoredCatalogEntry[] }
|
|
43
|
+
| { kind: 'localCatalogError'; data: LoadedModelPickerData; message: string }
|
|
44
|
+
| { kind: 'catalog'; provider: CloudProviderId; data: LoadedModelPickerData }
|
|
45
|
+
| { kind: 'keyEntry'; provider: CloudProviderId; action: 'set' | 'edit'; data: LoadedModelPickerData; submitting: boolean; error?: string }
|
|
46
|
+
| { kind: 'keyManage'; provider: CloudProviderId; data: LoadedModelPickerData; submitting: boolean; error?: string }
|
|
47
|
+
| { kind: 'oauthManage'; data: LoadedModelPickerData; submitting: boolean; error?: string }
|
|
48
|
+
| { kind: 'oauthLogin'; data: LoadedModelPickerData; phase: 'waiting' | 'exchanging' | 'error'; url?: string; message?: string }
|
|
49
|
+
| { kind: 'hfInput'; data: LoadedModelPickerData; error?: string }
|
|
50
|
+
| { kind: 'hfLoading'; data: LoadedModelPickerData; input: string }
|
|
51
|
+
| { kind: 'hfFilePick'; data: LoadedModelPickerData; input: string; repo: HuggingFaceRepoInfo; files: HuggingFaceSibling[] }
|
|
52
|
+
| { kind: 'hfReview'; data: LoadedModelPickerData; plan: HfDownloadPlan }
|
|
53
|
+
| { kind: 'hfDownloading'; data: LoadedModelPickerData; plan: HfDownloadPlan; progress: HfDownloadProgress }
|
|
54
|
+
| { kind: 'hfDone'; data: LoadedModelPickerData; model: LocalHfModel; alreadyInstalled?: boolean }
|
|
55
|
+
| { kind: 'hfError'; data: LoadedModelPickerData; message: string; input?: string }
|
|
56
|
+
| { kind: 'localUninstallPick'; data: LoadedModelPickerData }
|
|
57
|
+
| { kind: 'localUninstallConfirm'; data: LoadedModelPickerData; target: LocalUninstallTarget }
|
|
58
|
+
| { kind: 'localUninstalling'; data: LoadedModelPickerData; target: LocalUninstallTarget }
|
|
59
|
+
| { kind: 'localUninstallDone'; data: LoadedModelPickerData; modelName: string }
|
|
60
|
+
| { kind: 'localUninstallError'; data: LoadedModelPickerData; target: LocalUninstallTarget; message: string }
|
|
61
|
+
| { kind: 'localRunnerSetup'; data: LoadedModelPickerData; model: LocalHfModel }
|
|
62
|
+
| { kind: 'localRunnerInstalling'; data: LoadedModelPickerData; model: LocalHfModel; startedAt: number; progress: LlamaCppInstallProgress }
|
|
63
|
+
| { kind: 'localRunnerInstallFail'; data: LoadedModelPickerData; model: LocalHfModel; result: Extract<LlamaCppInstallResult, { ok: false }> }
|
|
64
|
+
| { kind: 'localRunnerPathEntry'; data: LoadedModelPickerData; model: LocalHfModel; submitting: boolean; error?: string }
|
|
65
|
+
| { kind: 'localRunnerStarting'; data: LoadedModelPickerData; model: LocalHfModel; startedAt: number }
|
|
66
|
+
| { kind: 'localRunnerStartFail'; data: LoadedModelPickerData; model: LocalHfModel; result: Extract<LlamaCppStartResult, { ok: false }> }
|
|
67
|
+
| { kind: 'mmprojOffer'; data: LoadedModelPickerData; model: LocalHfModel }
|
|
68
|
+
| { kind: 'mmprojDownloading'; data: LoadedModelPickerData; model: LocalHfModel; progress: HfDownloadProgress }
|
|
69
|
+
| { kind: 'mmprojError'; data: LoadedModelPickerData; model: LocalHfModel; message: string }
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ProviderId } from '../storage/config.js'
|
|
2
|
+
import { uninstallLocalHfModel } from './huggingface.js'
|
|
3
|
+
import { formatLocalHfModelDisplayName } from './modelDisplay.js'
|
|
4
|
+
import type { LoadedModelPickerData as LoadedData, LocalUninstallTarget, ModelPickerState as State } from './modelPickerTypes.js'
|
|
5
|
+
import { refreshLocalModelData } from './modelPickerData.js'
|
|
6
|
+
export function localUninstallTargets(data: LoadedData): LocalUninstallTarget[] {
|
|
7
|
+
return data.hfModels.map(model => ({
|
|
8
|
+
kind: 'hf' as const,
|
|
9
|
+
id: model.id,
|
|
10
|
+
displayName: formatLocalHfModelDisplayName(model.id, {
|
|
11
|
+
displayName: model.displayName,
|
|
12
|
+
maxLength: 64,
|
|
13
|
+
}),
|
|
14
|
+
sizeBytes: model.sizeBytes,
|
|
15
|
+
}))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isCurrentLocalUninstallTarget(
|
|
19
|
+
target: LocalUninstallTarget,
|
|
20
|
+
currentProvider: ProviderId,
|
|
21
|
+
currentModel: string,
|
|
22
|
+
): boolean {
|
|
23
|
+
return target.kind === 'hf' && currentProvider === 'llamacpp' && target.id === currentModel
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function localUninstallBoundaryCopy(_target: LocalUninstallTarget): string {
|
|
27
|
+
return 'This removes only the downloaded GGUF file and metadata from this machine.'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function uninstallLocalModel(
|
|
31
|
+
state: Extract<State, { kind: 'localUninstallConfirm' }>,
|
|
32
|
+
setState: (s: State) => void,
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
setState({ kind: 'localUninstalling', data: state.data, target: state.target })
|
|
35
|
+
const modelName = state.target.displayName
|
|
36
|
+
try {
|
|
37
|
+
await uninstallLocalHfModel(state.target.id)
|
|
38
|
+
const data = await refreshLocalModelData(state.data)
|
|
39
|
+
setState({ kind: 'localUninstallDone', data, modelName })
|
|
40
|
+
} catch (err: unknown) {
|
|
41
|
+
setState({
|
|
42
|
+
kind: 'localUninstallError',
|
|
43
|
+
data: state.data,
|
|
44
|
+
target: state.target,
|
|
45
|
+
message: (err as Error).message,
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { SelectOption } from '../ui/Select.js'
|
|
2
|
+
import type { ProviderId } from '../storage/config.js'
|
|
3
|
+
import { defaultModelFor, type EthagentConfig } from '../storage/config.js'
|
|
4
|
+
import type { LlamaCppInstallResult } from './llamacpp.js'
|
|
5
|
+
import { orderGgufFilesForSpec, type GgufMachineFit } from './modelRecommendation.js'
|
|
6
|
+
import {
|
|
7
|
+
localModelId,
|
|
8
|
+
type HuggingFaceRepoInfo,
|
|
9
|
+
type HuggingFaceSibling,
|
|
10
|
+
} from './huggingface.js'
|
|
11
|
+
import {
|
|
12
|
+
MODEL_PICKER_CLOUD_PROVIDERS,
|
|
13
|
+
orderModelsForContextFit,
|
|
14
|
+
type CloudProviderId,
|
|
15
|
+
type ModelPickerContextFit,
|
|
16
|
+
} from './modelPickerOptions.js'
|
|
17
|
+
import { formatModelDisplayName } from './modelDisplay.js'
|
|
18
|
+
import { contextFitLabel, formatBytes, modelMetadataSubtext } from './modelPickerDisplay.js'
|
|
19
|
+
import type { LoadedModelPickerData as LoadedData, ModelPickerSelection } from './modelPickerTypes.js'
|
|
20
|
+
import type { ModelCatalogResult } from './catalog.js'
|
|
21
|
+
import type { SpecSnapshot } from './runtimeDetection.js'
|
|
22
|
+
|
|
23
|
+
export function buildCatalogOptions(
|
|
24
|
+
provider: CloudProviderId,
|
|
25
|
+
catalog: ModelCatalogResult | undefined,
|
|
26
|
+
currentProvider: ProviderId,
|
|
27
|
+
currentModel: string,
|
|
28
|
+
contextFit?: ModelPickerContextFit | null,
|
|
29
|
+
): SelectOption<string>[] {
|
|
30
|
+
if (!catalog || catalog.entries.length === 0) {
|
|
31
|
+
return [{
|
|
32
|
+
value: `hdr:catalog-empty:${provider}`,
|
|
33
|
+
label: 'No Models Found',
|
|
34
|
+
disabled: true,
|
|
35
|
+
role: 'notice',
|
|
36
|
+
prefix: 'note',
|
|
37
|
+
}]
|
|
38
|
+
}
|
|
39
|
+
const sourceById = new Map(catalog.entries.map(entry => [entry.id, entry.source]))
|
|
40
|
+
return orderModelsForContextFit(provider, catalog.entries.map(entry => entry.id), contextFit).map(id => {
|
|
41
|
+
const active = currentProvider === provider && currentModel === id
|
|
42
|
+
const suffix = sourceById.get(id) === 'fallback' ? ' fallback' : ''
|
|
43
|
+
const displayName = formatModelDisplayName(provider, id, { maxLength: 64 })
|
|
44
|
+
return {
|
|
45
|
+
value: `full:${provider}:${id}`,
|
|
46
|
+
label: contextFitLabel(provider, id, `${displayName}${active ? ' *' : ''}${suffix}`, contextFit),
|
|
47
|
+
role: 'option',
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function parseCloudValue(value: string): { provider: CloudProviderId; model: string } | null {
|
|
53
|
+
if (!value.startsWith('c:')) return null
|
|
54
|
+
const rest = value.slice(2)
|
|
55
|
+
const sep = rest.indexOf(':')
|
|
56
|
+
if (sep === -1) return null
|
|
57
|
+
const provider = rest.slice(0, sep)
|
|
58
|
+
const model = rest.slice(sep + 1)
|
|
59
|
+
if (!isCloudProvider(provider) || !model) return null
|
|
60
|
+
return { provider, model }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function localModelOptionIndex(
|
|
64
|
+
options: SelectOption<string>[],
|
|
65
|
+
currentProvider: ProviderId,
|
|
66
|
+
currentModel: string,
|
|
67
|
+
): number {
|
|
68
|
+
return options.findIndex(opt => {
|
|
69
|
+
if (opt.disabled) return false
|
|
70
|
+
if (opt.value.startsWith('hf:')) return opt.value.slice(3) === currentModel && currentProvider === 'llamacpp'
|
|
71
|
+
if (opt.value.startsWith('uc:')) return opt.value.slice(3) === currentModel && currentProvider === 'llamacpp'
|
|
72
|
+
return false
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function localOrCloudOptionIndex(
|
|
77
|
+
options: SelectOption<string>[],
|
|
78
|
+
currentProvider: ProviderId,
|
|
79
|
+
currentModel: string,
|
|
80
|
+
): number {
|
|
81
|
+
return options.findIndex(opt => {
|
|
82
|
+
if (opt.disabled) return false
|
|
83
|
+
if (opt.value.startsWith('hf:')) return opt.value.slice(3) === currentModel && currentProvider === 'llamacpp'
|
|
84
|
+
if (opt.value.startsWith('uc:')) return opt.value.slice(3) === currentModel && currentProvider === 'llamacpp'
|
|
85
|
+
const cloud = parseCloudValue(opt.value)
|
|
86
|
+
return cloud?.provider === currentProvider && cloud.model === currentModel
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function parseFullCatalogValue(value: string): { provider: CloudProviderId; model: string } | null {
|
|
91
|
+
if (!value.startsWith('full:')) return null
|
|
92
|
+
const rest = value.slice(5)
|
|
93
|
+
const sep = rest.indexOf(':')
|
|
94
|
+
if (sep === -1) return null
|
|
95
|
+
const provider = rest.slice(0, sep)
|
|
96
|
+
const model = rest.slice(sep + 1)
|
|
97
|
+
if (!isCloudProvider(provider) || !model) return null
|
|
98
|
+
return { provider, model }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function parseKeyValue(value: string): { action: 'set' | 'edit' | 'manage'; provider: CloudProviderId } | null {
|
|
102
|
+
if (!value.startsWith('key:')) return null
|
|
103
|
+
const parts = value.split(':')
|
|
104
|
+
if (parts.length !== 3) return null
|
|
105
|
+
const action = parts[1]
|
|
106
|
+
const provider = parts[2]
|
|
107
|
+
if (action !== 'set' && action !== 'edit' && action !== 'manage') return null
|
|
108
|
+
if (!provider) return null
|
|
109
|
+
if (!isCloudProvider(provider)) return null
|
|
110
|
+
return { action, provider }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function pickFallbackSelection(data: LoadedData, removed: ProviderId): ModelPickerSelection | null {
|
|
114
|
+
for (const provider of MODEL_PICKER_CLOUD_PROVIDERS) {
|
|
115
|
+
if (provider === removed) continue
|
|
116
|
+
if (data.cloudKeys[provider] !== true) continue
|
|
117
|
+
const catalogModel = data.cloudCatalogs[provider]?.entries[0]?.id
|
|
118
|
+
const model = catalogModel ?? defaultModelFor(provider)
|
|
119
|
+
return { kind: 'cloud', provider, model, keyJustSet: false }
|
|
120
|
+
}
|
|
121
|
+
if (data.hfModels.length > 0) {
|
|
122
|
+
return { kind: 'llamacpp', model: data.hfModels[0]!.id }
|
|
123
|
+
}
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function configForProvider(config: EthagentConfig, provider: CloudProviderId): EthagentConfig {
|
|
128
|
+
return {
|
|
129
|
+
...config,
|
|
130
|
+
provider,
|
|
131
|
+
model: config.provider === provider ? config.model : defaultModelFor(provider),
|
|
132
|
+
baseUrl: provider === 'openai' && config.provider === 'openai' ? config.baseUrl : undefined,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function buildHfFileOptions(
|
|
137
|
+
repo: HuggingFaceRepoInfo,
|
|
138
|
+
files: HuggingFaceSibling[],
|
|
139
|
+
spec: SpecSnapshot | undefined,
|
|
140
|
+
installedModelIds: string[] = [],
|
|
141
|
+
): SelectOption<string>[] {
|
|
142
|
+
const ordered = spec
|
|
143
|
+
? orderGgufFilesForSpec(repo, files, spec)
|
|
144
|
+
: files.map(file => ({ file, fit: 'unknown' as GgufMachineFit, score: 0, budgetBytes: 0 }))
|
|
145
|
+
const recommended = spec ? ordered[0]?.file.filename : undefined
|
|
146
|
+
const installed = new Set(installedModelIds)
|
|
147
|
+
return ordered.map(item => {
|
|
148
|
+
const size = item.file.sizeBytes ? formatBytes(item.file.sizeBytes) : ''
|
|
149
|
+
const indicators = [
|
|
150
|
+
item.file.filename === recommended ? 'Recommended' : '',
|
|
151
|
+
installed.has(localModelId(repo.repoId, item.file.filename)) ? 'Installed' : '',
|
|
152
|
+
]
|
|
153
|
+
return {
|
|
154
|
+
value: item.file.filename,
|
|
155
|
+
label: item.file.filename,
|
|
156
|
+
subtext: modelMetadataSubtext(size, indicators),
|
|
157
|
+
role: 'option' as const,
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function buildRunnerRecoveryOptions(
|
|
163
|
+
_result: Extract<LlamaCppInstallResult, { ok: false }>,
|
|
164
|
+
): SelectOption<'stop-and-retry' | 'path' | 'back'>[] {
|
|
165
|
+
return [
|
|
166
|
+
{ value: 'stop-and-retry', label: 'Stop and Retry', hint: 'Stop background runners and try the install again' },
|
|
167
|
+
{ value: 'path', label: 'Use Existing Runner Path' },
|
|
168
|
+
{ value: 'back', label: 'Back To Picker' },
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isCloudProvider(value: string): value is CloudProviderId {
|
|
173
|
+
return MODEL_PICKER_CLOUD_PROVIDERS.includes(value as CloudProviderId)
|
|
174
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { ProviderId } from '../storage/config.js'
|
|
2
|
-
import type { Message,
|
|
2
|
+
import type { Message, Provider, ProviderCompleteOptions, StreamEvent } from './contracts.js'
|
|
3
3
|
import { ProviderError } from './contracts.js'
|
|
4
4
|
import { providerErrorFromResponse } from './errors.js'
|
|
5
5
|
import { fetchWithRetryStreamEvents } from './retry.js'
|
|
6
6
|
import { iterSseFrames } from './sse.js'
|
|
7
|
-
import {
|
|
8
|
-
import { hasImageBlocks, ImageLoadError, loadImageBlock } from '../utils/images.js'
|
|
7
|
+
import { hasImageBlocks, ImageLoadError } from '../utils/images.js'
|
|
9
8
|
import { providerDisplayName } from '../models/providerDisplay.js'
|
|
9
|
+
import { toWireMessages } from './openaiChatWire.js'
|
|
10
|
+
|
|
11
|
+
export { toWireMessages } from './openaiChatWire.js'
|
|
10
12
|
|
|
11
13
|
export type OpenAIToolDefinition = {
|
|
12
14
|
type: 'function'
|
|
@@ -270,91 +272,6 @@ export class OpenAIChatProvider implements Provider {
|
|
|
270
272
|
|
|
271
273
|
}
|
|
272
274
|
|
|
273
|
-
export async function toWireMessages(messages: Message[]): Promise<Array<Record<string, unknown>>> {
|
|
274
|
-
const out: Array<Record<string, unknown>> = []
|
|
275
|
-
|
|
276
|
-
for (const message of messages) {
|
|
277
|
-
if (typeof message.content === 'string') {
|
|
278
|
-
out.push({ role: message.role, content: message.content })
|
|
279
|
-
continue
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (message.role === 'user') {
|
|
283
|
-
const toolResults = message.content.filter(isToolResultBlock)
|
|
284
|
-
if (toolResults.length > 0) {
|
|
285
|
-
for (const block of toolResults) {
|
|
286
|
-
out.push({
|
|
287
|
-
role: 'tool',
|
|
288
|
-
tool_call_id: block.toolUseId,
|
|
289
|
-
content: block.content,
|
|
290
|
-
})
|
|
291
|
-
}
|
|
292
|
-
const nonToolBlocks = message.content.filter(block => block.type !== 'tool_result')
|
|
293
|
-
if (nonToolBlocks.length > 0) {
|
|
294
|
-
out.push({ role: 'user', content: await toOpenAIUserContent(nonToolBlocks) })
|
|
295
|
-
}
|
|
296
|
-
continue
|
|
297
|
-
}
|
|
298
|
-
out.push({ role: 'user', content: await toOpenAIUserContent(message.content) })
|
|
299
|
-
continue
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (message.role === 'assistant') {
|
|
303
|
-
const textParts = message.content.filter(isTextBlock).map(block => block.text)
|
|
304
|
-
const toolCalls = message.content.filter(isToolUseBlock).map(block => ({
|
|
305
|
-
id: block.id,
|
|
306
|
-
type: 'function',
|
|
307
|
-
function: {
|
|
308
|
-
name: block.name,
|
|
309
|
-
arguments: JSON.stringify(block.input),
|
|
310
|
-
},
|
|
311
|
-
}))
|
|
312
|
-
out.push({
|
|
313
|
-
role: 'assistant',
|
|
314
|
-
content: textParts.join(''),
|
|
315
|
-
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
316
|
-
})
|
|
317
|
-
continue
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const toolResults = message.content.filter(isToolResultBlock)
|
|
321
|
-
if (toolResults.length > 0) {
|
|
322
|
-
for (const block of toolResults) {
|
|
323
|
-
out.push({
|
|
324
|
-
role: 'tool',
|
|
325
|
-
tool_call_id: block.toolUseId,
|
|
326
|
-
content: block.content,
|
|
327
|
-
})
|
|
328
|
-
}
|
|
329
|
-
continue
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
out.push({ role: message.role, content: messageTextContent(message) })
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return normalizeSystemMessages(out)
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
async function toOpenAIUserContent(blocks: MessageContentBlock[]): Promise<Array<Record<string, unknown>>> {
|
|
339
|
-
const parts: Array<Record<string, unknown>> = []
|
|
340
|
-
for (const block of blocks) {
|
|
341
|
-
if (block.type === 'text') {
|
|
342
|
-
if (block.text.length > 0) parts.push({ type: 'text', text: block.text })
|
|
343
|
-
continue
|
|
344
|
-
}
|
|
345
|
-
if (block.type === 'image') {
|
|
346
|
-
const loaded = await loadImageBlock(block)
|
|
347
|
-
if (loaded.url) {
|
|
348
|
-
parts.push({ type: 'image_url', image_url: { url: loaded.url } })
|
|
349
|
-
} else if (loaded.dataBase64 && loaded.mimeType) {
|
|
350
|
-
parts.push({ type: 'image_url', image_url: { url: `data:${loaded.mimeType};base64,${loaded.dataBase64}` } })
|
|
351
|
-
}
|
|
352
|
-
continue
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
return parts.length > 0 ? parts : [{ type: 'text', text: '' }]
|
|
356
|
-
}
|
|
357
|
-
|
|
358
275
|
export function supportsOpenAIImages(model: string): boolean {
|
|
359
276
|
const normalized = model.toLowerCase()
|
|
360
277
|
if (normalized.includes('gpt-3.5')) return false
|
|
@@ -366,42 +283,6 @@ export function localModelNameHintsVision(model: string): boolean {
|
|
|
366
283
|
return /llava|bakllava|qwen[-_.]?vl|qwen2[-_.]?vl|qwen2\.5[-_.]?vl|minicpm-?v|llama-3\.2.*vision|mllama|cogvlm|internvl|moondream|pixtral|phi-?3[\.-]?vision|phi-?3\.5[\.-]?vision|smolvlm/.test(normalized)
|
|
367
284
|
}
|
|
368
285
|
|
|
369
|
-
function normalizeSystemMessages(messages: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
|
|
370
|
-
const systemContents: string[] = []
|
|
371
|
-
const nonSystem: Array<Record<string, unknown>> = []
|
|
372
|
-
|
|
373
|
-
for (const message of messages) {
|
|
374
|
-
if (message.role === 'system') {
|
|
375
|
-
if (typeof message.content === 'string' && message.content.length > 0) {
|
|
376
|
-
systemContents.push(message.content)
|
|
377
|
-
}
|
|
378
|
-
continue
|
|
379
|
-
}
|
|
380
|
-
nonSystem.push(message)
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (systemContents.length === 0) return nonSystem
|
|
384
|
-
return [
|
|
385
|
-
{
|
|
386
|
-
role: 'system',
|
|
387
|
-
content: systemContents.join('\n\n'),
|
|
388
|
-
},
|
|
389
|
-
...nonSystem,
|
|
390
|
-
]
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function isTextBlock(block: MessageContentBlock): block is Extract<MessageContentBlock, { type: 'text' }> {
|
|
394
|
-
return block.type === 'text'
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function isToolUseBlock(block: MessageContentBlock): block is Extract<MessageContentBlock, { type: 'tool_use' }> {
|
|
398
|
-
return block.type === 'tool_use'
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function isToolResultBlock(block: MessageContentBlock): block is Extract<MessageContentBlock, { type: 'tool_result' }> {
|
|
402
|
-
return block.type === 'tool_result'
|
|
403
|
-
}
|
|
404
|
-
|
|
405
286
|
function parseToolArguments(inputJson: string): Record<string, unknown> {
|
|
406
287
|
if (!inputJson.trim()) return {}
|
|
407
288
|
const direct = tryParseJsonOnce(inputJson)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Message, MessageContentBlock } from './contracts.js'
|
|
2
|
+
import { messageTextContent } from '../utils/messages.js'
|
|
3
|
+
import { loadImageBlock } from '../utils/images.js'
|
|
4
|
+
|
|
5
|
+
export async function toWireMessages(messages: Message[]): Promise<Array<Record<string, unknown>>> {
|
|
6
|
+
const out: Array<Record<string, unknown>> = []
|
|
7
|
+
|
|
8
|
+
for (const message of messages) {
|
|
9
|
+
if (typeof message.content === 'string') {
|
|
10
|
+
out.push({ role: message.role, content: message.content })
|
|
11
|
+
continue
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (message.role === 'user') {
|
|
15
|
+
const toolResults = message.content.filter(isToolResultBlock)
|
|
16
|
+
if (toolResults.length > 0) {
|
|
17
|
+
for (const block of toolResults) {
|
|
18
|
+
out.push({
|
|
19
|
+
role: 'tool',
|
|
20
|
+
tool_call_id: block.toolUseId,
|
|
21
|
+
content: block.content,
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
const nonToolBlocks = message.content.filter(block => block.type !== 'tool_result')
|
|
25
|
+
if (nonToolBlocks.length > 0) {
|
|
26
|
+
out.push({ role: 'user', content: await toOpenAIUserContent(nonToolBlocks) })
|
|
27
|
+
}
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
out.push({ role: 'user', content: await toOpenAIUserContent(message.content) })
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (message.role === 'assistant') {
|
|
35
|
+
const textParts = message.content.filter(isTextBlock).map(block => block.text)
|
|
36
|
+
const toolCalls = message.content.filter(isToolUseBlock).map(block => ({
|
|
37
|
+
id: block.id,
|
|
38
|
+
type: 'function',
|
|
39
|
+
function: {
|
|
40
|
+
name: block.name,
|
|
41
|
+
arguments: JSON.stringify(block.input),
|
|
42
|
+
},
|
|
43
|
+
}))
|
|
44
|
+
out.push({
|
|
45
|
+
role: 'assistant',
|
|
46
|
+
content: textParts.join(''),
|
|
47
|
+
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
48
|
+
})
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const toolResults = message.content.filter(isToolResultBlock)
|
|
53
|
+
if (toolResults.length > 0) {
|
|
54
|
+
for (const block of toolResults) {
|
|
55
|
+
out.push({
|
|
56
|
+
role: 'tool',
|
|
57
|
+
tool_call_id: block.toolUseId,
|
|
58
|
+
content: block.content,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
out.push({ role: message.role, content: messageTextContent(message) })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return normalizeSystemMessages(out)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function toOpenAIUserContent(blocks: MessageContentBlock[]): Promise<Array<Record<string, unknown>>> {
|
|
71
|
+
const parts: Array<Record<string, unknown>> = []
|
|
72
|
+
for (const block of blocks) {
|
|
73
|
+
if (block.type === 'text') {
|
|
74
|
+
if (block.text.length > 0) parts.push({ type: 'text', text: block.text })
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
if (block.type === 'image') {
|
|
78
|
+
const loaded = await loadImageBlock(block)
|
|
79
|
+
if (loaded.url) {
|
|
80
|
+
parts.push({ type: 'image_url', image_url: { url: loaded.url } })
|
|
81
|
+
} else if (loaded.dataBase64 && loaded.mimeType) {
|
|
82
|
+
parts.push({ type: 'image_url', image_url: { url: `data:${loaded.mimeType};base64,${loaded.dataBase64}` } })
|
|
83
|
+
}
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return parts.length > 0 ? parts : [{ type: 'text', text: '' }]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeSystemMessages(messages: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
|
|
91
|
+
const systemContents: string[] = []
|
|
92
|
+
const nonSystem: Array<Record<string, unknown>> = []
|
|
93
|
+
|
|
94
|
+
for (const message of messages) {
|
|
95
|
+
if (message.role === 'system') {
|
|
96
|
+
if (typeof message.content === 'string' && message.content.length > 0) {
|
|
97
|
+
systemContents.push(message.content)
|
|
98
|
+
}
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
nonSystem.push(message)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (systemContents.length === 0) return nonSystem
|
|
105
|
+
return [
|
|
106
|
+
{
|
|
107
|
+
role: 'system',
|
|
108
|
+
content: systemContents.join('\n\n'),
|
|
109
|
+
},
|
|
110
|
+
...nonSystem,
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isTextBlock(block: MessageContentBlock): block is Extract<MessageContentBlock, { type: 'text' }> {
|
|
115
|
+
return block.type === 'text'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isToolUseBlock(block: MessageContentBlock): block is Extract<MessageContentBlock, { type: 'tool_use' }> {
|
|
119
|
+
return block.type === 'tool_use'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isToolResultBlock(block: MessageContentBlock): block is Extract<MessageContentBlock, { type: 'tool_result' }> {
|
|
123
|
+
return block.type === 'tool_result'
|
|
124
|
+
}
|