mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
fix(frontend): hide api key field for openhands provider and auto-populate the key (#11791)
This commit is contained in:
parent
3504ca7752
commit
b830d1c513
@ -97,6 +97,10 @@ class SaasSettingsStore(SettingsStore):
|
||||
return settings
|
||||
|
||||
async def store(self, item: Settings):
|
||||
# Check if provider is OpenHands and generate API key if needed
|
||||
if item and self._is_openhands_provider(item):
|
||||
await self._ensure_openhands_api_key(item)
|
||||
|
||||
with self.session_maker() as session:
|
||||
existing = None
|
||||
kwargs = {}
|
||||
@ -368,6 +372,30 @@ class SaasSettingsStore(SettingsStore):
|
||||
def _should_encrypt(self, key: str) -> bool:
|
||||
return key in ('llm_api_key', 'llm_api_key_for_byor', 'search_api_key')
|
||||
|
||||
def _is_openhands_provider(self, item: Settings) -> bool:
|
||||
"""Check if the settings use the OpenHands provider."""
|
||||
return bool(item.llm_model and item.llm_model.startswith('openhands/'))
|
||||
|
||||
async def _ensure_openhands_api_key(self, item: Settings) -> None:
|
||||
"""Generate and set the OpenHands API key for the given settings.
|
||||
|
||||
First checks if an existing key with the OpenHands alias exists,
|
||||
and reuses it if found. Otherwise, generates a new key.
|
||||
"""
|
||||
# Generate new key if none exists
|
||||
generated_key = await self._generate_openhands_key()
|
||||
if generated_key:
|
||||
item.llm_api_key = SecretStr(generated_key)
|
||||
logger.info(
|
||||
'saas_settings_store:store:generated_openhands_key',
|
||||
extra={'user_id': self.user_id},
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
'saas_settings_store:store:failed_to_generate_openhands_key',
|
||||
extra={'user_id': self.user_id},
|
||||
)
|
||||
|
||||
async def _create_user_in_lite_llm(
|
||||
self, client: httpx.AsyncClient, email: str | None, max_budget: int, spend: int
|
||||
):
|
||||
@ -390,3 +418,55 @@ class SaasSettingsStore(SettingsStore):
|
||||
},
|
||||
)
|
||||
return response
|
||||
|
||||
async def _generate_openhands_key(self) -> str | None:
|
||||
"""Generate a new OpenHands provider key for a user."""
|
||||
if not (LITE_LLM_API_KEY and LITE_LLM_API_URL):
|
||||
logger.warning(
|
||||
'saas_settings_store:_generate_openhands_key:litellm_config_not_found',
|
||||
extra={'user_id': self.user_id},
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(
|
||||
verify=httpx_verify_option(),
|
||||
headers={
|
||||
'x-goog-api-key': LITE_LLM_API_KEY,
|
||||
},
|
||||
) as client:
|
||||
response = await client.post(
|
||||
f'{LITE_LLM_API_URL}/key/generate',
|
||||
json={
|
||||
'user_id': self.user_id,
|
||||
'metadata': {'type': 'openhands'},
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
response_json = response.json()
|
||||
key = response_json.get('key')
|
||||
|
||||
if key:
|
||||
logger.info(
|
||||
'saas_settings_store:_generate_openhands_key:success',
|
||||
extra={
|
||||
'user_id': self.user_id,
|
||||
'key_length': len(key) if key else 0,
|
||||
'key_prefix': (
|
||||
key[:10] + '...' if key and len(key) > 10 else key
|
||||
),
|
||||
},
|
||||
)
|
||||
return key
|
||||
else:
|
||||
logger.error(
|
||||
'saas_settings_store:_generate_openhands_key:no_key_in_response',
|
||||
extra={'user_id': self.user_id, 'response_json': response_json},
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
'saas_settings_store:_generate_openhands_key:error',
|
||||
extra={'user_id': self.user_id, 'error': str(e)},
|
||||
)
|
||||
return None
|
||||
|
||||
@ -21,7 +21,11 @@ interface ModelSelectorProps {
|
||||
isDisabled?: boolean;
|
||||
models: Record<string, { separator: string; models: string[] }>;
|
||||
currentModel?: string;
|
||||
onChange?: (model: string | null) => void;
|
||||
onChange?: (provider: string | null, model: string | null) => void;
|
||||
onDefaultValuesChanged?: (
|
||||
provider: string | null,
|
||||
model: string | null,
|
||||
) => void;
|
||||
wrapperClassName?: string;
|
||||
labelClassName?: string;
|
||||
}
|
||||
@ -31,6 +35,7 @@ export function ModelSelector({
|
||||
models,
|
||||
currentModel,
|
||||
onChange,
|
||||
onDefaultValuesChanged,
|
||||
wrapperClassName,
|
||||
labelClassName,
|
||||
}: ModelSelectorProps) {
|
||||
@ -56,6 +61,7 @@ export function ModelSelector({
|
||||
setLitellmId(currentModel);
|
||||
setSelectedProvider(provider);
|
||||
setSelectedModel(model);
|
||||
onDefaultValuesChanged?.(provider, model);
|
||||
}
|
||||
}, [currentModel]);
|
||||
|
||||
@ -65,6 +71,7 @@ export function ModelSelector({
|
||||
|
||||
const separator = models[provider]?.separator || "";
|
||||
setLitellmId(provider + separator);
|
||||
onChange?.(provider, null);
|
||||
};
|
||||
|
||||
const handleChangeModel = (model: string) => {
|
||||
@ -76,7 +83,7 @@ export function ModelSelector({
|
||||
}
|
||||
setLitellmId(fullModel);
|
||||
setSelectedModel(model);
|
||||
onChange?.(fullModel);
|
||||
onChange?.(selectedProvider, model);
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
|
||||
@ -102,10 +102,22 @@ function LlmSettingsScreen() {
|
||||
: (settings?.SECURITY_ANALYZER ?? DEFAULT_SETTINGS.SECURITY_ANALYZER),
|
||||
);
|
||||
|
||||
const [selectedProvider, setSelectedProvider] = React.useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const modelsAndProviders = organizeModelsAndProviders(
|
||||
resources?.models || [],
|
||||
);
|
||||
|
||||
// Determine if we should hide the API key input and use OpenHands-managed key (when using OpenHands provider in SaaS mode)
|
||||
const currentModel = currentSelectedModel || settings?.LLM_MODEL;
|
||||
const isOpenHandsProvider =
|
||||
(view === "basic" && selectedProvider === "openhands") ||
|
||||
(view === "advanced" && currentModel?.startsWith("openhands/"));
|
||||
const isSaasMode = config?.APP_MODE === "saas";
|
||||
const shouldUseOpenHandsKey = isOpenHandsProvider && isSaasMode;
|
||||
|
||||
React.useEffect(() => {
|
||||
const determineWhetherToToggleAdvancedSettings = () => {
|
||||
if (resources && settings) {
|
||||
@ -196,10 +208,13 @@ function LlmSettingsScreen() {
|
||||
|
||||
const fullLlmModel = provider && model && `${provider}/${model}`;
|
||||
|
||||
// Use OpenHands-managed key for OpenHands provider in SaaS mode
|
||||
const finalApiKey = shouldUseOpenHandsKey ? null : apiKey;
|
||||
|
||||
saveSettings(
|
||||
{
|
||||
LLM_MODEL: fullLlmModel,
|
||||
llm_api_key: apiKey || null,
|
||||
llm_api_key: finalApiKey || null,
|
||||
SEARCH_API_KEY: searchApiKey || "",
|
||||
CONFIRMATION_MODE: confirmationMode,
|
||||
SECURITY_ANALYZER:
|
||||
@ -244,11 +259,14 @@ function LlmSettingsScreen() {
|
||||
.get("security-analyzer-input")
|
||||
?.toString();
|
||||
|
||||
// Use OpenHands-managed key for OpenHands provider in SaaS mode
|
||||
const finalApiKey = shouldUseOpenHandsKey ? null : apiKey;
|
||||
|
||||
saveSettings(
|
||||
{
|
||||
LLM_MODEL: model,
|
||||
LLM_BASE_URL: baseUrl,
|
||||
llm_api_key: apiKey || null,
|
||||
llm_api_key: finalApiKey || null,
|
||||
SEARCH_API_KEY: searchApiKey || "",
|
||||
AGENT: agent,
|
||||
CONFIRMATION_MODE: confirmationMode,
|
||||
@ -282,7 +300,10 @@ function LlmSettingsScreen() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleModelIsDirty = (model: string | null) => {
|
||||
const handleModelIsDirty = (
|
||||
provider: string | null,
|
||||
model: string | null,
|
||||
) => {
|
||||
// openai providers are special case; see ModelSelector
|
||||
// component for details
|
||||
const modelIsDirty = model !== settings?.LLM_MODEL.replace("openai/", "");
|
||||
@ -293,6 +314,15 @@ function LlmSettingsScreen() {
|
||||
|
||||
// Track the currently selected model for help text display
|
||||
setCurrentSelectedModel(model);
|
||||
setSelectedProvider(provider);
|
||||
};
|
||||
|
||||
const onDefaultValuesChanged = (
|
||||
provider: string | null,
|
||||
model: string | null,
|
||||
) => {
|
||||
setSelectedProvider(provider);
|
||||
setCurrentSelectedModel(model);
|
||||
};
|
||||
|
||||
const handleApiKeyIsDirty = (apiKey: string) => {
|
||||
@ -463,6 +493,7 @@ function LlmSettingsScreen() {
|
||||
models={modelsAndProviders}
|
||||
currentModel={settings.LLM_MODEL || DEFAULT_OPENHANDS_MODEL}
|
||||
onChange={handleModelIsDirty}
|
||||
onDefaultValuesChanged={onDefaultValuesChanged}
|
||||
wrapperClassName="!flex-col !gap-6"
|
||||
/>
|
||||
{(settings.LLM_MODEL?.startsWith("openhands/") ||
|
||||
@ -472,27 +503,31 @@ function LlmSettingsScreen() {
|
||||
</>
|
||||
)}
|
||||
|
||||
<SettingsInput
|
||||
testId="llm-api-key-input"
|
||||
name="llm-api-key-input"
|
||||
label={t(I18nKey.SETTINGS_FORM$API_KEY)}
|
||||
type="password"
|
||||
className="w-full max-w-[680px]"
|
||||
placeholder={settings.LLM_API_KEY_SET ? "<hidden>" : ""}
|
||||
onChange={handleApiKeyIsDirty}
|
||||
startContent={
|
||||
settings.LLM_API_KEY_SET && (
|
||||
<KeyStatusIcon isSet={settings.LLM_API_KEY_SET} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
{!shouldUseOpenHandsKey && (
|
||||
<>
|
||||
<SettingsInput
|
||||
testId="llm-api-key-input"
|
||||
name="llm-api-key-input"
|
||||
label={t(I18nKey.SETTINGS_FORM$API_KEY)}
|
||||
type="password"
|
||||
className="w-full max-w-[680px]"
|
||||
placeholder={settings.LLM_API_KEY_SET ? "<hidden>" : ""}
|
||||
onChange={handleApiKeyIsDirty}
|
||||
startContent={
|
||||
settings.LLM_API_KEY_SET && (
|
||||
<KeyStatusIcon isSet={settings.LLM_API_KEY_SET} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<HelpLink
|
||||
testId="llm-api-key-help-anchor"
|
||||
text={t(I18nKey.SETTINGS$DONT_KNOW_API_KEY)}
|
||||
linkText={t(I18nKey.SETTINGS$CLICK_FOR_INSTRUCTIONS)}
|
||||
href="https://docs.all-hands.dev/usage/local-setup#getting-an-api-key"
|
||||
/>
|
||||
<HelpLink
|
||||
testId="llm-api-key-help-anchor"
|
||||
text={t(I18nKey.SETTINGS$DONT_KNOW_API_KEY)}
|
||||
linkText={t(I18nKey.SETTINGS$CLICK_FOR_INSTRUCTIONS)}
|
||||
href="https://docs.all-hands.dev/usage/local-setup#getting-an-api-key"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -527,26 +562,30 @@ function LlmSettingsScreen() {
|
||||
onChange={handleBaseUrlIsDirty}
|
||||
/>
|
||||
|
||||
<SettingsInput
|
||||
testId="llm-api-key-input"
|
||||
name="llm-api-key-input"
|
||||
label={t(I18nKey.SETTINGS_FORM$API_KEY)}
|
||||
type="password"
|
||||
className="w-full max-w-[680px]"
|
||||
placeholder={settings.LLM_API_KEY_SET ? "<hidden>" : ""}
|
||||
onChange={handleApiKeyIsDirty}
|
||||
startContent={
|
||||
settings.LLM_API_KEY_SET && (
|
||||
<KeyStatusIcon isSet={settings.LLM_API_KEY_SET} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<HelpLink
|
||||
testId="llm-api-key-help-anchor-advanced"
|
||||
text={t(I18nKey.SETTINGS$DONT_KNOW_API_KEY)}
|
||||
linkText={t(I18nKey.SETTINGS$CLICK_FOR_INSTRUCTIONS)}
|
||||
href="https://docs.all-hands.dev/usage/local-setup#getting-an-api-key"
|
||||
/>
|
||||
{!shouldUseOpenHandsKey && (
|
||||
<>
|
||||
<SettingsInput
|
||||
testId="llm-api-key-input"
|
||||
name="llm-api-key-input"
|
||||
label={t(I18nKey.SETTINGS_FORM$API_KEY)}
|
||||
type="password"
|
||||
className="w-full max-w-[680px]"
|
||||
placeholder={settings.LLM_API_KEY_SET ? "<hidden>" : ""}
|
||||
onChange={handleApiKeyIsDirty}
|
||||
startContent={
|
||||
settings.LLM_API_KEY_SET && (
|
||||
<KeyStatusIcon isSet={settings.LLM_API_KEY_SET} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<HelpLink
|
||||
testId="llm-api-key-help-anchor-advanced"
|
||||
text={t(I18nKey.SETTINGS$DONT_KNOW_API_KEY)}
|
||||
linkText={t(I18nKey.SETTINGS$CLICK_FOR_INSTRUCTIONS)}
|
||||
href="https://docs.all-hands.dev/usage/local-setup#getting-an-api-key"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{config?.APP_MODE !== "saas" && (
|
||||
<>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user