fix(frontend): hide api key field for openhands provider and auto-populate the key (#11791)

This commit is contained in:
Hiep Le 2025-11-24 20:44:15 +07:00 committed by GitHub
parent 3504ca7752
commit b830d1c513
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 171 additions and 45 deletions

View File

@ -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

View File

@ -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 = () => {

View File

@ -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" && (
<>