ethagent 2.1.1 → 2.2.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/package.json +1 -1
- package/src/auth/openaiOAuth/credentials.ts +47 -0
- package/src/auth/openaiOAuth/crypto.ts +23 -0
- package/src/auth/openaiOAuth/index.ts +238 -0
- package/src/auth/openaiOAuth/landingPage.ts +125 -0
- package/src/auth/openaiOAuth/listener.ts +151 -0
- package/src/auth/openaiOAuth/refresh.ts +70 -0
- package/src/auth/openaiOAuth/shared.ts +115 -0
- package/src/chat/chatSessionState.ts +2 -1
- package/src/chat/commands.ts +2 -1
- package/src/identity/ens/agentRecords.ts +5 -19
- package/src/identity/ens/ensAutomation/setup.ts +0 -1
- package/src/identity/ens/ensAutomation/types.ts +0 -1
- package/src/identity/hub/OperationalRoutes.tsx +2 -11
- package/src/identity/hub/components/IdentitySummary.tsx +8 -3
- package/src/identity/hub/components/MenuScreen.tsx +1 -2
- package/src/identity/hub/components/menuFlagsFromReconciliation.ts +1 -3
- package/src/identity/hub/effects/ens/transactions.ts +15 -15
- package/src/identity/hub/effects/index.ts +0 -1
- package/src/identity/hub/effects/profile/profileState.ts +12 -4
- package/src/identity/hub/effects/publicProfile/runPublicProfileSave.ts +37 -159
- package/src/identity/hub/effects/rebackup/runRebackup.ts +2 -2
- package/src/identity/hub/effects/restoreAdmin.ts +2 -61
- package/src/identity/hub/effects/shared/sync.ts +3 -44
- package/src/identity/hub/flows/custody/CustodyEditFlow.tsx +1 -39
- package/src/identity/hub/flows/custody/custodyFlowActions.ts +5 -3
- package/src/identity/hub/flows/custody/custodyFlowTypes.ts +1 -1
- package/src/identity/hub/flows/ens/EnsEditAdvancedScreens.tsx +80 -175
- package/src/identity/hub/flows/ens/EnsEditFlow.tsx +20 -75
- package/src/identity/hub/flows/ens/EnsEditMaintenanceScreens.tsx +16 -56
- package/src/identity/hub/flows/ens/EnsEditReviewScreens.tsx +0 -18
- package/src/identity/hub/flows/ens/EnsEditRunners.tsx +0 -136
- package/src/identity/hub/flows/ens/EnsEditShared.tsx +5 -4
- package/src/identity/hub/flows/ens/EnsEditSimpleScreens.tsx +56 -205
- package/src/identity/hub/flows/ens/IdentityHubEnsFlow.tsx +7 -0
- package/src/identity/hub/flows/ens/OperatorWalletsScreen.tsx +0 -31
- package/src/identity/hub/flows/ens/ensEditCopy.ts +1 -1
- package/src/identity/hub/flows/ens/ensEditTypes.ts +6 -20
- package/src/identity/hub/flows/profile/EditProfileFlow.tsx +7 -0
- package/src/identity/hub/flows/restore/RestoreFlow.tsx +5 -5
- package/src/identity/hub/reconciliation/agentReconciliation/hook.ts +0 -1
- package/src/identity/hub/reconciliation/agentReconciliation/run.ts +1 -34
- package/src/identity/hub/reconciliation/agentReconciliation/types.ts +0 -4
- package/src/identity/hub/reconciliation/index.ts +0 -7
- package/src/identity/hub/reconciliation/walletSetup.ts +1 -194
- package/src/identity/wallet/browserWallet/types.ts +0 -5
- package/src/identity/wallet/page/copy.ts +1 -31
- package/src/identity/wallet/walletPurposeCompat.ts +0 -2
- package/src/models/ModelPicker.tsx +246 -8
- package/src/models/catalog.ts +28 -1
- package/src/models/modelPickerOptions.ts +15 -1
- package/src/providers/openai-responses-format.ts +156 -0
- package/src/providers/openai-responses.ts +276 -0
- package/src/providers/registry.ts +85 -8
- package/src/runtime/systemPrompt.ts +1 -1
- package/src/runtime/turn.ts +0 -1
- package/src/storage/secrets.ts +4 -1
- package/src/tools/privateContinuityEditTool.ts +6 -0
- package/src/utils/openExternal.ts +20 -10
- package/src/identity/ens/ensRegistration.ts +0 -199
|
@@ -25,8 +25,11 @@ import {
|
|
|
25
25
|
type GgufMachineFit,
|
|
26
26
|
} from './modelRecommendation.js'
|
|
27
27
|
import { hasKey, rmKey, setKey } from '../storage/secrets.js'
|
|
28
|
+
import { OpenAIOAuthService } from '../auth/openaiOAuth/index.js'
|
|
29
|
+
import { hasOpenAIOAuthCredentials, rmOpenAIOAuthCredentials } from '../auth/openaiOAuth/credentials.js'
|
|
30
|
+
import { openExternalUrl } from '../utils/openExternal.js'
|
|
28
31
|
import { defaultModelFor, type EthagentConfig, type ProviderId } from '../storage/config.js'
|
|
29
|
-
import { clearModelCatalogCache, discoverProviderModels, type ModelCatalogResult } from './catalog.js'
|
|
32
|
+
import { clearModelCatalogCache, discoverProviderModels, isOpenAIOAuthAllowedModel, OPENAI_OAUTH_DEFAULT_MODEL, type ModelCatalogResult } from './catalog.js'
|
|
30
33
|
import { contextWindowInfo } from '../runtime/compaction.js'
|
|
31
34
|
import {
|
|
32
35
|
createHfDownloadPlan,
|
|
@@ -56,6 +59,7 @@ import {
|
|
|
56
59
|
LOCAL_MODEL_LINK_HINT,
|
|
57
60
|
MODEL_PICKER_CLOUD_PROVIDERS,
|
|
58
61
|
orderModelsForContextFit,
|
|
62
|
+
type CloudCredentialKind,
|
|
59
63
|
type CloudProviderId,
|
|
60
64
|
type ModelPickerContextFit,
|
|
61
65
|
type ModelPickerOptionsData,
|
|
@@ -89,6 +93,8 @@ type State =
|
|
|
89
93
|
| { kind: 'catalog'; provider: CloudProviderId; data: LoadedData }
|
|
90
94
|
| { kind: 'keyEntry'; provider: CloudProviderId; action: 'set' | 'edit'; data: LoadedData; submitting: boolean; error?: string }
|
|
91
95
|
| { kind: 'keyManage'; provider: CloudProviderId; data: LoadedData; submitting: boolean; error?: string }
|
|
96
|
+
| { kind: 'oauthManage'; data: LoadedData; submitting: boolean; error?: string }
|
|
97
|
+
| { kind: 'oauthLogin'; data: LoadedData; phase: 'waiting' | 'exchanging' | 'error'; url?: string; message?: string }
|
|
92
98
|
| { kind: 'hfInput'; data: LoadedData; error?: string }
|
|
93
99
|
| { kind: 'hfLoading'; data: LoadedData; input: string }
|
|
94
100
|
| { kind: 'hfFilePick'; data: LoadedData; input: string; repo: HuggingFaceRepoInfo; files: HuggingFaceSibling[] }
|
|
@@ -119,18 +125,29 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
119
125
|
}) => {
|
|
120
126
|
const [state, setState] = useState<State>({ kind: 'loading' })
|
|
121
127
|
const hfAbortRef = useRef<AbortController | null>(null)
|
|
128
|
+
const oauthServiceRef = useRef<OpenAIOAuthService | null>(null)
|
|
122
129
|
|
|
123
130
|
useEffect(() => {
|
|
124
131
|
let cancelled = false
|
|
125
132
|
void (async () => {
|
|
126
|
-
const [llamaCpp, hfModels, machineSpec, keyEntries] = await Promise.all([
|
|
133
|
+
const [llamaCpp, hfModels, machineSpec, keyEntries, openaiOauth] = await Promise.all([
|
|
127
134
|
probeLlamaCpp(),
|
|
128
135
|
loadHfPickerModels(),
|
|
129
136
|
detectSpec(),
|
|
130
137
|
Promise.all(MODEL_PICKER_CLOUD_PROVIDERS.map(async p => [p, await hasKey(p)] as const)),
|
|
138
|
+
hasOpenAIOAuthCredentials(),
|
|
131
139
|
])
|
|
132
140
|
if (cancelled) return
|
|
133
|
-
const
|
|
141
|
+
const rawCloudKeys = Object.fromEntries(keyEntries) as Partial<Record<ProviderId, boolean>>
|
|
142
|
+
const cloudKeys: Partial<Record<ProviderId, boolean>> = {
|
|
143
|
+
...rawCloudKeys,
|
|
144
|
+
openai: rawCloudKeys.openai === true || openaiOauth,
|
|
145
|
+
}
|
|
146
|
+
const cloudCredentialKinds: Partial<Record<ProviderId, CloudCredentialKind>> = {}
|
|
147
|
+
if (openaiOauth) cloudCredentialKinds.openai = 'oauth'
|
|
148
|
+
else if (rawCloudKeys.openai === true) cloudCredentialKinds.openai = 'apikey'
|
|
149
|
+
if (rawCloudKeys.anthropic === true) cloudCredentialKinds.anthropic = 'apikey'
|
|
150
|
+
if (rawCloudKeys.gemini === true) cloudCredentialKinds.gemini = 'apikey'
|
|
134
151
|
const catalogEntries = await Promise.all(
|
|
135
152
|
MODEL_PICKER_CLOUD_PROVIDERS
|
|
136
153
|
.filter(provider => cloudKeys[provider])
|
|
@@ -144,6 +161,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
144
161
|
machineSpec,
|
|
145
162
|
cloudKeys,
|
|
146
163
|
cloudCatalogs,
|
|
164
|
+
cloudCredentialKinds,
|
|
147
165
|
}
|
|
148
166
|
if (featuredHfRepo) {
|
|
149
167
|
const installedFeatured = await findInstalledHfModelForInput(featuredHfRepo)
|
|
@@ -163,6 +181,8 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
163
181
|
|
|
164
182
|
useEffect(() => () => {
|
|
165
183
|
hfAbortRef.current?.abort()
|
|
184
|
+
oauthServiceRef.current?.cleanup()
|
|
185
|
+
oauthServiceRef.current = null
|
|
166
186
|
}, [])
|
|
167
187
|
|
|
168
188
|
if (state.kind === 'loading') {
|
|
@@ -569,7 +589,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
569
589
|
setState({ kind: 'list', data: state.data })
|
|
570
590
|
return
|
|
571
591
|
}
|
|
572
|
-
void deleteKey(state, currentConfig, setState)
|
|
592
|
+
void deleteKey(state, currentConfig, setState, onPick, currentProvider)
|
|
573
593
|
}}
|
|
574
594
|
onCancel={() => setState({ kind: 'list', data: state.data })}
|
|
575
595
|
/>
|
|
@@ -579,6 +599,106 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
579
599
|
)
|
|
580
600
|
}
|
|
581
601
|
|
|
602
|
+
if (state.kind === 'oauthManage') {
|
|
603
|
+
const { submitting, error } = state
|
|
604
|
+
return (
|
|
605
|
+
<Surface
|
|
606
|
+
title="ChatGPT Sign-in"
|
|
607
|
+
subtitle="Manage your ChatGPT sign-in."
|
|
608
|
+
footer="enter select · esc back"
|
|
609
|
+
>
|
|
610
|
+
{submitting ? (
|
|
611
|
+
<Spinner label="signing out..." />
|
|
612
|
+
) : (
|
|
613
|
+
<Select
|
|
614
|
+
options={[
|
|
615
|
+
{ value: 'signin', label: 'Sign in Again' },
|
|
616
|
+
{ value: 'signout', label: 'Sign Out' },
|
|
617
|
+
{ value: 'cancel', label: 'Back' },
|
|
618
|
+
]}
|
|
619
|
+
onSubmit={(value) => {
|
|
620
|
+
if (value === 'signin') {
|
|
621
|
+
void startOpenAIOAuthFlow(state.data, currentConfig, setState, oauthServiceRef, onPick)
|
|
622
|
+
return
|
|
623
|
+
}
|
|
624
|
+
if (value === 'cancel') {
|
|
625
|
+
setState({ kind: 'list', data: state.data })
|
|
626
|
+
return
|
|
627
|
+
}
|
|
628
|
+
void signOutOAuth(state, currentConfig, setState, onPick, currentProvider)
|
|
629
|
+
}}
|
|
630
|
+
onCancel={() => setState({ kind: 'list', data: state.data })}
|
|
631
|
+
/>
|
|
632
|
+
)}
|
|
633
|
+
{error ? <Text color={theme.accentError}>{error}</Text> : null}
|
|
634
|
+
</Surface>
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (state.kind === 'oauthLogin') {
|
|
639
|
+
if (state.phase === 'error') {
|
|
640
|
+
return (
|
|
641
|
+
<Surface
|
|
642
|
+
title="OpenAI Sign-in Failed"
|
|
643
|
+
subtitle={state.message ?? 'Sign-in did not complete.'}
|
|
644
|
+
tone="error"
|
|
645
|
+
footer="enter select · esc back"
|
|
646
|
+
>
|
|
647
|
+
<Select<'retry' | 'apikey' | 'back'>
|
|
648
|
+
options={[
|
|
649
|
+
{ value: 'retry', label: 'Try Again' },
|
|
650
|
+
{ value: 'apikey', label: 'Add API Key Instead' },
|
|
651
|
+
{ value: 'back', label: 'Back To Picker' },
|
|
652
|
+
]}
|
|
653
|
+
onSubmit={choice => {
|
|
654
|
+
if (choice === 'retry') void startOpenAIOAuthFlow(state.data, currentConfig, setState, oauthServiceRef, onPick)
|
|
655
|
+
else if (choice === 'apikey') setState({ kind: 'keyEntry', provider: 'openai', action: 'set', data: state.data, submitting: false })
|
|
656
|
+
else setState({ kind: 'list', data: state.data })
|
|
657
|
+
}}
|
|
658
|
+
onCancel={() => setState({ kind: 'list', data: state.data })}
|
|
659
|
+
/>
|
|
660
|
+
</Surface>
|
|
661
|
+
)
|
|
662
|
+
}
|
|
663
|
+
if (state.phase === 'exchanging') {
|
|
664
|
+
return (
|
|
665
|
+
<Surface title="Finishing OpenAI Sign-in" subtitle="Exchanging credentials with auth.openai.com.">
|
|
666
|
+
<Spinner label="completing sign-in..." />
|
|
667
|
+
</Surface>
|
|
668
|
+
)
|
|
669
|
+
}
|
|
670
|
+
return (
|
|
671
|
+
<Surface
|
|
672
|
+
title="Sign in with ChatGPT"
|
|
673
|
+
subtitle="Opened your browser to auth.openai.com. Approve to continue."
|
|
674
|
+
footer="esc cancel"
|
|
675
|
+
>
|
|
676
|
+
<Spinner label="waiting for browser sign-in..." />
|
|
677
|
+
{state.url ? (
|
|
678
|
+
<Box flexDirection="column" marginTop={1}>
|
|
679
|
+
<Text color={theme.dim}>If the browser did not open, visit:</Text>
|
|
680
|
+
<Text color={theme.dim}>{state.url}</Text>
|
|
681
|
+
</Box>
|
|
682
|
+
) : null}
|
|
683
|
+
<Box marginTop={1}>
|
|
684
|
+
<Select<'cancel'>
|
|
685
|
+
options={[{ value: 'cancel', label: 'Cancel Sign-in' }]}
|
|
686
|
+
onSubmit={() => {
|
|
687
|
+
oauthServiceRef.current?.cleanup()
|
|
688
|
+
oauthServiceRef.current = null
|
|
689
|
+
setState({ kind: 'list', data: state.data })
|
|
690
|
+
}}
|
|
691
|
+
onCancel={() => {
|
|
692
|
+
oauthServiceRef.current?.cleanup()
|
|
693
|
+
oauthServiceRef.current = null
|
|
694
|
+
setState({ kind: 'list', data: state.data })
|
|
695
|
+
}}
|
|
696
|
+
/>
|
|
697
|
+
</Box>
|
|
698
|
+
</Surface>
|
|
699
|
+
)
|
|
700
|
+
}
|
|
701
|
+
|
|
582
702
|
if (state.kind === 'catalog') {
|
|
583
703
|
const catalog = state.data.cloudCatalogs[state.provider]
|
|
584
704
|
const options = buildCatalogOptions(state.provider, catalog, currentProvider, currentModel, contextFit)
|
|
@@ -648,7 +768,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
648
768
|
options={options}
|
|
649
769
|
initialIndex={initialIndex === -1 ? 0 : initialIndex}
|
|
650
770
|
maxVisible={12}
|
|
651
|
-
onSubmit={(value) => handleSubmit(value, state, setState, onPick, onCancel)}
|
|
771
|
+
onSubmit={(value) => handleSubmit(value, state, setState, onPick, onCancel, currentConfig, oauthServiceRef)}
|
|
652
772
|
onCancel={() => setState({ kind: 'list', data: state.data })}
|
|
653
773
|
/>
|
|
654
774
|
</Surface>
|
|
@@ -669,7 +789,7 @@ export const ModelPicker: React.FC<ModelPickerProps> = ({
|
|
|
669
789
|
options={options}
|
|
670
790
|
initialIndex={initialIndex === -1 ? 0 : initialIndex}
|
|
671
791
|
maxVisible={10}
|
|
672
|
-
onSubmit={(value) => handleSubmit(value, state, setState, onPick, onCancel)}
|
|
792
|
+
onSubmit={(value) => handleSubmit(value, state, setState, onPick, onCancel, currentConfig, oauthServiceRef)}
|
|
673
793
|
onCancel={onCancel}
|
|
674
794
|
/>
|
|
675
795
|
</Surface>
|
|
@@ -682,6 +802,8 @@ function handleSubmit(
|
|
|
682
802
|
setState: (s: State) => void,
|
|
683
803
|
onPick: (sel: ModelPickerSelection) => void,
|
|
684
804
|
onCancel: () => void,
|
|
805
|
+
currentConfig: EthagentConfig,
|
|
806
|
+
oauthServiceRef: React.MutableRefObject<OpenAIOAuthService | null>,
|
|
685
807
|
): void {
|
|
686
808
|
if (value.startsWith('hdr:')) return
|
|
687
809
|
if (value === 'cancel') {
|
|
@@ -727,12 +849,20 @@ function handleSubmit(
|
|
|
727
849
|
const parsed = parseKeyValue(value)
|
|
728
850
|
if (!parsed) return
|
|
729
851
|
if (parsed.action === 'manage') {
|
|
852
|
+
if (parsed.provider === 'openai' && state.data.cloudCredentialKinds?.openai === 'oauth') {
|
|
853
|
+
setState({ kind: 'oauthManage', data: state.data, submitting: false })
|
|
854
|
+
return
|
|
855
|
+
}
|
|
730
856
|
setState({ kind: 'keyManage', provider: parsed.provider, data: state.data, submitting: false })
|
|
731
857
|
return
|
|
732
858
|
}
|
|
733
859
|
setState({ kind: 'keyEntry', provider: parsed.provider, action: parsed.action, data: state.data, submitting: false })
|
|
734
860
|
return
|
|
735
861
|
}
|
|
862
|
+
if (value === 'oauth:openai') {
|
|
863
|
+
void startOpenAIOAuthFlow(state.data, currentConfig, setState, oauthServiceRef, onPick)
|
|
864
|
+
return
|
|
865
|
+
}
|
|
736
866
|
if (value.startsWith('catalog:')) {
|
|
737
867
|
const provider = value.slice('catalog:'.length)
|
|
738
868
|
if (isCloudProvider(provider)) setState({ kind: 'catalog', provider, data: state.data })
|
|
@@ -908,36 +1038,144 @@ async function submitKey(
|
|
|
908
1038
|
}
|
|
909
1039
|
}
|
|
910
1040
|
|
|
1041
|
+
async function startOpenAIOAuthFlow(
|
|
1042
|
+
data: LoadedData,
|
|
1043
|
+
currentConfig: EthagentConfig,
|
|
1044
|
+
setState: (s: State) => void,
|
|
1045
|
+
serviceRef: React.MutableRefObject<OpenAIOAuthService | null>,
|
|
1046
|
+
onPick: (sel: ModelPickerSelection) => void,
|
|
1047
|
+
): Promise<void> {
|
|
1048
|
+
serviceRef.current?.cleanup()
|
|
1049
|
+
const service = new OpenAIOAuthService()
|
|
1050
|
+
serviceRef.current = service
|
|
1051
|
+
setState({ kind: 'oauthLogin', data, phase: 'waiting' })
|
|
1052
|
+
try {
|
|
1053
|
+
const result = await service.start(authUrl => {
|
|
1054
|
+
openExternalUrl(authUrl)
|
|
1055
|
+
setState({ kind: 'oauthLogin', data, phase: 'waiting', url: authUrl })
|
|
1056
|
+
})
|
|
1057
|
+
if (serviceRef.current !== service) return
|
|
1058
|
+
setState({ kind: 'oauthLogin', data, phase: 'exchanging' })
|
|
1059
|
+
if (result.kind === 'apikey') {
|
|
1060
|
+
if (typeof result.apiKey !== 'string' || result.apiKey.length === 0) {
|
|
1061
|
+
throw new Error(`OAuth result was apikey kind but apiKey is ${typeof result.apiKey}; refusing to store.`)
|
|
1062
|
+
}
|
|
1063
|
+
try {
|
|
1064
|
+
await setKey('openai', result.apiKey)
|
|
1065
|
+
} catch (err) {
|
|
1066
|
+
throw new Error(`Storing the OpenAI API key failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
let refreshed: LoadedData
|
|
1070
|
+
try {
|
|
1071
|
+
refreshed = await refreshProviderKeyState(data, currentConfig, 'openai')
|
|
1072
|
+
} catch (err) {
|
|
1073
|
+
throw new Error(`Refreshing the OpenAI provider state failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
1074
|
+
}
|
|
1075
|
+
if (serviceRef.current !== service) return
|
|
1076
|
+
serviceRef.current = null
|
|
1077
|
+
if (result.kind === 'oauth-only' && !isOpenAIOAuthAllowedModel(currentConfig.model)) {
|
|
1078
|
+
onPick({ kind: 'cloud', provider: 'openai', model: OPENAI_OAUTH_DEFAULT_MODEL, keyJustSet: true })
|
|
1079
|
+
return
|
|
1080
|
+
}
|
|
1081
|
+
setState({ kind: 'list', data: refreshed })
|
|
1082
|
+
} catch (err: unknown) {
|
|
1083
|
+
if (serviceRef.current !== service) return
|
|
1084
|
+
serviceRef.current = null
|
|
1085
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
1086
|
+
if (message === 'OpenAI sign-in was cancelled.') {
|
|
1087
|
+
setState({ kind: 'list', data })
|
|
1088
|
+
return
|
|
1089
|
+
}
|
|
1090
|
+
setState({ kind: 'oauthLogin', data, phase: 'error', message })
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
911
1094
|
async function deleteKey(
|
|
912
1095
|
state: Extract<State, { kind: 'keyManage' }>,
|
|
913
1096
|
currentConfig: EthagentConfig,
|
|
914
1097
|
setState: (s: State) => void,
|
|
1098
|
+
onPick: (sel: ModelPickerSelection) => void,
|
|
1099
|
+
currentProvider: ProviderId,
|
|
915
1100
|
): Promise<void> {
|
|
916
1101
|
setState({ ...state, submitting: true, error: undefined })
|
|
917
1102
|
try {
|
|
918
1103
|
await rmKey(state.provider)
|
|
1104
|
+
if (state.provider === 'openai') await rmOpenAIOAuthCredentials()
|
|
919
1105
|
const data = await refreshProviderKeyState(state.data, currentConfig, state.provider)
|
|
1106
|
+
if (currentProvider === state.provider) {
|
|
1107
|
+
const fallback = pickFallbackSelection(data, state.provider)
|
|
1108
|
+
if (fallback) {
|
|
1109
|
+
onPick(fallback)
|
|
1110
|
+
return
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
setState({ kind: 'list', data })
|
|
1114
|
+
} catch (err: unknown) {
|
|
1115
|
+
setState({ ...state, submitting: false, error: (err as Error).message })
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
async function signOutOAuth(
|
|
1120
|
+
state: Extract<State, { kind: 'oauthManage' }>,
|
|
1121
|
+
currentConfig: EthagentConfig,
|
|
1122
|
+
setState: (s: State) => void,
|
|
1123
|
+
onPick: (sel: ModelPickerSelection) => void,
|
|
1124
|
+
currentProvider: ProviderId,
|
|
1125
|
+
): Promise<void> {
|
|
1126
|
+
setState({ ...state, submitting: true, error: undefined })
|
|
1127
|
+
try {
|
|
1128
|
+
await rmKey('openai')
|
|
1129
|
+
await rmOpenAIOAuthCredentials()
|
|
1130
|
+
const data = await refreshProviderKeyState(state.data, currentConfig, 'openai')
|
|
1131
|
+
if (currentProvider === 'openai') {
|
|
1132
|
+
const fallback = pickFallbackSelection(data, 'openai')
|
|
1133
|
+
if (fallback) {
|
|
1134
|
+
onPick(fallback)
|
|
1135
|
+
return
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
920
1138
|
setState({ kind: 'list', data })
|
|
921
1139
|
} catch (err: unknown) {
|
|
922
1140
|
setState({ ...state, submitting: false, error: (err as Error).message })
|
|
923
1141
|
}
|
|
924
1142
|
}
|
|
925
1143
|
|
|
1144
|
+
function pickFallbackSelection(data: LoadedData, removed: ProviderId): ModelPickerSelection | null {
|
|
1145
|
+
for (const provider of MODEL_PICKER_CLOUD_PROVIDERS) {
|
|
1146
|
+
if (provider === removed) continue
|
|
1147
|
+
if (data.cloudKeys[provider] !== true) continue
|
|
1148
|
+
const catalogModel = data.cloudCatalogs[provider]?.entries[0]?.id
|
|
1149
|
+
const model = catalogModel ?? defaultModelFor(provider)
|
|
1150
|
+
return { kind: 'cloud', provider, model, keyJustSet: false }
|
|
1151
|
+
}
|
|
1152
|
+
if (data.hfModels.length > 0) {
|
|
1153
|
+
return { kind: 'llamacpp', model: data.hfModels[0]!.id }
|
|
1154
|
+
}
|
|
1155
|
+
return null
|
|
1156
|
+
}
|
|
1157
|
+
|
|
926
1158
|
async function refreshProviderKeyState(
|
|
927
1159
|
data: LoadedData,
|
|
928
1160
|
currentConfig: EthagentConfig,
|
|
929
1161
|
provider: CloudProviderId,
|
|
930
1162
|
): Promise<LoadedData> {
|
|
931
1163
|
clearModelCatalogCache()
|
|
932
|
-
const
|
|
1164
|
+
const apiKeySet = await hasKey(provider)
|
|
1165
|
+
const oauthSet = provider === 'openai' ? await hasOpenAIOAuthCredentials() : false
|
|
1166
|
+
const keySet = apiKeySet || oauthSet
|
|
933
1167
|
const cloudKeys = { ...data.cloudKeys, [provider]: keySet }
|
|
1168
|
+
const cloudCredentialKinds: Partial<Record<ProviderId, CloudCredentialKind>> = { ...(data.cloudCredentialKinds ?? {}) }
|
|
1169
|
+
if (oauthSet) cloudCredentialKinds[provider] = 'oauth'
|
|
1170
|
+
else if (apiKeySet) cloudCredentialKinds[provider] = 'apikey'
|
|
1171
|
+
else delete cloudCredentialKinds[provider]
|
|
934
1172
|
const cloudCatalogs = { ...data.cloudCatalogs }
|
|
935
1173
|
if (keySet) {
|
|
936
1174
|
cloudCatalogs[provider] = await discoverProviderModels(configForProvider(currentConfig, provider))
|
|
937
1175
|
} else {
|
|
938
1176
|
delete cloudCatalogs[provider]
|
|
939
1177
|
}
|
|
940
|
-
return { ...data, cloudKeys, cloudCatalogs }
|
|
1178
|
+
return { ...data, cloudKeys, cloudCatalogs, cloudCredentialKinds }
|
|
941
1179
|
}
|
|
942
1180
|
|
|
943
1181
|
function configForProvider(config: EthagentConfig, provider: CloudProviderId): EthagentConfig {
|
package/src/models/catalog.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { defaultModelFor, type EthagentConfig, type ProviderId } from '../storage/config.js'
|
|
2
2
|
import { getKey } from '../storage/secrets.js'
|
|
3
3
|
import { loadLocalHfModels } from './huggingface.js'
|
|
4
|
+
import { hasOpenAIOAuthCredentials } from '../auth/openaiOAuth/credentials.js'
|
|
5
|
+
|
|
6
|
+
const OPENAI_OAUTH_MODEL_IDS = ['gpt-5.5', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex', 'gpt-5.2'] as const
|
|
7
|
+
|
|
8
|
+
export const OPENAI_OAUTH_DEFAULT_MODEL = 'gpt-5.4'
|
|
9
|
+
|
|
10
|
+
export function isOpenAIOAuthAllowedModel(model: string): boolean {
|
|
11
|
+
return (OPENAI_OAUTH_MODEL_IDS as readonly string[]).includes(model)
|
|
12
|
+
}
|
|
4
13
|
|
|
5
14
|
export type ModelCatalogSource = 'installed' | 'discovered' | 'fallback'
|
|
6
15
|
|
|
@@ -72,7 +81,12 @@ export async function discoverProviderModels(
|
|
|
72
81
|
|
|
73
82
|
const loadKey = deps.loadKey ?? getKey
|
|
74
83
|
const apiKey = await loadKey(provider)
|
|
75
|
-
if (!apiKey)
|
|
84
|
+
if (!apiKey) {
|
|
85
|
+
if (provider === 'openai' && await hasOpenAIOAuthCredentials()) {
|
|
86
|
+
return openAIOAuthCatalog()
|
|
87
|
+
}
|
|
88
|
+
return fallbackResult(config, `missing ${provider} API key`)
|
|
89
|
+
}
|
|
76
90
|
|
|
77
91
|
const baseUrl = provider === 'openai' ? openAIBaseUrlFor(config) : ''
|
|
78
92
|
const key = cacheKey(provider, baseUrl, true)
|
|
@@ -98,6 +112,19 @@ export async function discoverProviderModels(
|
|
|
98
112
|
}
|
|
99
113
|
}
|
|
100
114
|
|
|
115
|
+
function openAIOAuthCatalog(): ModelCatalogResult {
|
|
116
|
+
return {
|
|
117
|
+
provider: 'openai',
|
|
118
|
+
status: 'ok',
|
|
119
|
+
entries: OPENAI_OAUTH_MODEL_IDS.map(id => ({
|
|
120
|
+
provider: 'openai' as ProviderId,
|
|
121
|
+
id,
|
|
122
|
+
label: id,
|
|
123
|
+
source: 'discovered' as const,
|
|
124
|
+
})),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
101
128
|
function fallbackResult(config: EthagentConfig, error?: string): ModelCatalogResult {
|
|
102
129
|
const provider = config.provider
|
|
103
130
|
return {
|
|
@@ -22,6 +22,11 @@ export function cloudProviderDisplayName(provider: CloudProviderId): string {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export function providerDisplayName(provider: ProviderId): string {
|
|
26
|
+
if (provider === 'llamacpp') return 'llama.cpp'
|
|
27
|
+
return cloudProviderDisplayName(provider)
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
export type LocalHfPickerModel = {
|
|
26
31
|
id: string
|
|
27
32
|
displayName: string
|
|
@@ -32,6 +37,8 @@ export type LocalHfPickerModel = {
|
|
|
32
37
|
status: 'ready' | 'incomplete'
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
export type CloudCredentialKind = 'apikey' | 'oauth'
|
|
41
|
+
|
|
35
42
|
export type ModelPickerOptionsData = {
|
|
36
43
|
llamaCpp: {
|
|
37
44
|
binaryPresent: boolean
|
|
@@ -42,6 +49,7 @@ export type ModelPickerOptionsData = {
|
|
|
42
49
|
machineSpec?: SpecSnapshot
|
|
43
50
|
cloudKeys: Partial<Record<ProviderId, boolean>>
|
|
44
51
|
cloudCatalogs: Partial<Record<ProviderId, ModelCatalogResult>>
|
|
52
|
+
cloudCredentialKinds?: Partial<Record<ProviderId, CloudCredentialKind>>
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
export type ModelPickerContextFit = {
|
|
@@ -78,6 +86,9 @@ export function buildModelPickerOptions(
|
|
|
78
86
|
options.push(groupOption(`hdr:cloud:${provider}`, cloudProviderDisplayName(provider)))
|
|
79
87
|
const keySet = data.cloudKeys[provider] === true
|
|
80
88
|
if (!keySet) {
|
|
89
|
+
if (provider === 'openai') {
|
|
90
|
+
options.push(utilityOption('oauth:openai', 'Sign in with ChatGPT', 'Use your ChatGPT subscription'))
|
|
91
|
+
}
|
|
81
92
|
options.push(utilityOption(`key:set:${provider}`, 'Add API Key'))
|
|
82
93
|
continue
|
|
83
94
|
}
|
|
@@ -105,7 +116,10 @@ export function buildModelPickerOptions(
|
|
|
105
116
|
))
|
|
106
117
|
}
|
|
107
118
|
options.push(utilityOption(`catalog:${provider}`, 'Full Catalog'))
|
|
108
|
-
|
|
119
|
+
const manageLabel = provider === 'openai' && data.cloudCredentialKinds?.openai === 'oauth'
|
|
120
|
+
? 'Manage ChatGPT Sign-in'
|
|
121
|
+
: 'Manage API Key'
|
|
122
|
+
options.push(utilityOption(`key:manage:${provider}`, manageLabel))
|
|
109
123
|
}
|
|
110
124
|
|
|
111
125
|
options.push(sectionOption('hdr:exit', 'Exit'))
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { Message, MessageContentBlock } from './contracts.js'
|
|
2
|
+
import { messageTextContent } from '../utils/messages.js'
|
|
3
|
+
import type { OpenAIToolDefinition } from './openai-chat.js'
|
|
4
|
+
|
|
5
|
+
export type ResponsesInputContent =
|
|
6
|
+
| { type: 'input_text'; text: string }
|
|
7
|
+
| { type: 'output_text'; text: string }
|
|
8
|
+
|
|
9
|
+
export type ResponsesInputItem =
|
|
10
|
+
| { type: 'message'; role: 'user' | 'assistant'; content: ResponsesInputContent[] }
|
|
11
|
+
| { type: 'function_call'; id?: string; call_id: string; name: string; arguments: string }
|
|
12
|
+
| { type: 'function_call_output'; call_id: string; output: string }
|
|
13
|
+
|
|
14
|
+
export type ResponsesTool = {
|
|
15
|
+
type: 'function'
|
|
16
|
+
name: string
|
|
17
|
+
description: string
|
|
18
|
+
parameters: Record<string, unknown>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ResponsesRequestBody = {
|
|
22
|
+
model: string
|
|
23
|
+
input: ResponsesInputItem[]
|
|
24
|
+
instructions?: string
|
|
25
|
+
tools?: ResponsesTool[]
|
|
26
|
+
tool_choice?: 'auto' | 'none' | 'required'
|
|
27
|
+
parallel_tool_calls?: boolean
|
|
28
|
+
stream: true
|
|
29
|
+
store: false
|
|
30
|
+
max_output_tokens?: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function buildResponsesBody(args: {
|
|
34
|
+
model: string
|
|
35
|
+
messages: Message[]
|
|
36
|
+
tools: OpenAIToolDefinition[]
|
|
37
|
+
maxOutputTokens?: number
|
|
38
|
+
}): ResponsesRequestBody {
|
|
39
|
+
const { instructions, items } = splitMessages(args.messages)
|
|
40
|
+
const body: ResponsesRequestBody = {
|
|
41
|
+
model: args.model,
|
|
42
|
+
input: items,
|
|
43
|
+
stream: true,
|
|
44
|
+
store: false,
|
|
45
|
+
}
|
|
46
|
+
if (instructions) body.instructions = instructions
|
|
47
|
+
if (args.tools.length > 0) {
|
|
48
|
+
body.tools = args.tools.map(tool => ({
|
|
49
|
+
type: 'function' as const,
|
|
50
|
+
name: tool.function.name,
|
|
51
|
+
description: tool.function.description,
|
|
52
|
+
parameters: tool.function.parameters as Record<string, unknown>,
|
|
53
|
+
}))
|
|
54
|
+
body.parallel_tool_calls = true
|
|
55
|
+
body.tool_choice = 'auto'
|
|
56
|
+
}
|
|
57
|
+
if (args.maxOutputTokens !== undefined) {
|
|
58
|
+
body.max_output_tokens = args.maxOutputTokens
|
|
59
|
+
}
|
|
60
|
+
return body
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function splitMessages(messages: Message[]): {
|
|
64
|
+
instructions?: string
|
|
65
|
+
items: ResponsesInputItem[]
|
|
66
|
+
} {
|
|
67
|
+
const instructions: string[] = []
|
|
68
|
+
const items: ResponsesInputItem[] = []
|
|
69
|
+
|
|
70
|
+
for (const message of messages) {
|
|
71
|
+
if (message.role === 'system') {
|
|
72
|
+
const text = typeof message.content === 'string'
|
|
73
|
+
? message.content
|
|
74
|
+
: messageTextContent(message)
|
|
75
|
+
if (text) instructions.push(text)
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (message.role === 'user') {
|
|
80
|
+
const blocks = normalizeBlocks(message.content)
|
|
81
|
+
const toolResults = blocks.filter(isToolResultBlock)
|
|
82
|
+
if (toolResults.length > 0) {
|
|
83
|
+
for (const block of toolResults) {
|
|
84
|
+
items.push({
|
|
85
|
+
type: 'function_call_output',
|
|
86
|
+
call_id: block.toolUseId,
|
|
87
|
+
output: block.content,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
const remainingText = blocks
|
|
91
|
+
.filter(isTextBlock)
|
|
92
|
+
.map(block => block.text)
|
|
93
|
+
.join('')
|
|
94
|
+
if (remainingText) {
|
|
95
|
+
items.push({
|
|
96
|
+
type: 'message',
|
|
97
|
+
role: 'user',
|
|
98
|
+
content: [{ type: 'input_text', text: remainingText }],
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
const text = blocks.filter(isTextBlock).map(block => block.text).join('')
|
|
104
|
+
if (text) {
|
|
105
|
+
items.push({
|
|
106
|
+
type: 'message',
|
|
107
|
+
role: 'user',
|
|
108
|
+
content: [{ type: 'input_text', text }],
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const blocks = normalizeBlocks(message.content)
|
|
115
|
+
const text = blocks.filter(isTextBlock).map(block => block.text).join('')
|
|
116
|
+
if (text) {
|
|
117
|
+
items.push({
|
|
118
|
+
type: 'message',
|
|
119
|
+
role: 'assistant',
|
|
120
|
+
content: [{ type: 'output_text', text }],
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
for (const block of blocks.filter(isToolUseBlock)) {
|
|
124
|
+
items.push({
|
|
125
|
+
type: 'function_call',
|
|
126
|
+
call_id: block.id,
|
|
127
|
+
name: block.name,
|
|
128
|
+
arguments: JSON.stringify(block.input),
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
instructions: instructions.length > 0 ? instructions.join('\n\n') : undefined,
|
|
135
|
+
items,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizeBlocks(content: Message['content']): MessageContentBlock[] {
|
|
140
|
+
if (typeof content === 'string') {
|
|
141
|
+
return content ? [{ type: 'text', text: content }] : []
|
|
142
|
+
}
|
|
143
|
+
return content
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isTextBlock(block: MessageContentBlock): block is Extract<MessageContentBlock, { type: 'text' }> {
|
|
147
|
+
return block.type === 'text'
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isToolUseBlock(block: MessageContentBlock): block is Extract<MessageContentBlock, { type: 'tool_use' }> {
|
|
151
|
+
return block.type === 'tool_use'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isToolResultBlock(block: MessageContentBlock): block is Extract<MessageContentBlock, { type: 'tool_result' }> {
|
|
155
|
+
return block.type === 'tool_result'
|
|
156
|
+
}
|