feat: integrate LLM profiles into settings#210
Conversation
- Add LlmProfilesManager component below LLM settings form - Add profiles API service for CRUD operations (/api/profiles) - Add query hook (useLlmProfiles) and mutation hooks (save, delete, rename) - Add ProfileNameInput with placeholder and isOptional props - Add 'Add Profile' flow with optional profile name input - Add 'Edit Profile' flow from profile context menu - Auto-save profiles when saving LLM settings with profile name input shown - Derive profile name from model if not provided - Add SETTINGS$PROFILE_LOADED i18n key with all 15 language translations The implementation uses the SDK's /api/profiles endpoints and does not include activate functionality as the SDK doesn't support it.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Add tests for: - derive-profile-name utility (16 tests) - ProfilesService API (8 tests) - useLlmProfiles query hook (6 tests) - useSaveLlmProfile mutation hook (5 tests) - useDeleteLlmProfile mutation hook (4 tests) - useRenameLlmProfile mutation hook (5 tests) - ProfileNameInput component (14 tests) - ProfileRow component (11 tests) - ProfilesBody component (8 tests) - ProfileActionsMenu component (10 tests) - RenameProfileModal component (10 tests) - DeleteProfileModal component (8 tests) - LlmProfilesManager component (13 tests) Total: 118 new tests covering the full LLM profiles feature.
- Add visual wrapper (border + background) around profile name input - Add hint text explaining to configure settings and save - Add scroll behavior to show profile name input when Add Profile is clicked - Add SETTINGS$PROFILE_SAVE_HINT translation key for all 15 languages
Key changes: - Default view now shows profiles list instead of settings form - Settings form only appears when clicking 'Add LLM Profile' button - Add 'Active' badge to indicate which profile matches current settings - Add 'Activate' action in profile menu to switch to a profile - New components: LlmProfilesListView, ProfileListRow, ProfileListActionsMenu - Updated tests to reflect new profiles-first behavior - Added translations for: AVAILABLE_PROFILES, EDIT_LLM_PROFILE, PROFILE_ACTIVATE, PROFILE_EDIT, PROFILE_ACTIVATED, PROFILE_ACTIVE, PROFILE_RENAME, PROFILE_DELETE UI design matches the reference screenshots from openhands-ui.
Key changes: - Add CurrentSettingsRow component to display existing settings as active - Current settings row shows model, API key badge, and Active badge - Current settings appear first in the list when configured - Added onEditCurrentSettings handler to allow editing current config - Pass hasApiKey prop to show API key status - Fixed button styling (removed ml-auto for better layout) - Added CURRENT_CONFIGURATION translation key This ensures users always see their existing LLM configuration even if they haven't saved any profiles yet.
This update assumes SDK support for active_profile tracking (see OpenHands/software-agent-sdk#3172). Key changes: - ProfileListResponse now includes active_profile field - Added activateProfile endpoint and useActivateLlmProfile hook - LlmProfilesListView uses active_profile from API for active state - Removed CurrentSettingsRow workaround (no longer needed) - Simplified LlmProfilesListView props (data comes from useLlmProfiles) - Updated all tests to include active_profile in mocked responses The UI now properly tracks which profile is active via the backend, matching the behavior of the OpenHands main app.
- Add initialValuesOverride prop to SdkSectionPage
- When adding a new profile, pass {} to start with blank fields
- When editing an existing profile, load values from settings (default)
- Fix skeleton display logic to allow empty initialValuesOverride
Tests:
- Add test for empty profile name when adding new profile
- Add test for populated profile name when editing existing profile
- Add test for cancel button returning to profiles list
…erve API key on edit - Track original profile name when editing to detect renames - When editing and changing the profile name, call renameProfile API first before saving the updated config instead of creating a new profile - When editing without providing a new API key, fetch the existing profile's encrypted API key and include it in the save request to preserve it - Add ProfileExposeSecretsMode type and exposeSecrets parameter to getProfile for fetching encrypted secrets that can be round-tripped Co-authored-by: openhands <openhands@all-hands.dev>
When activating an LLM profile, the SettingsService internal cache (settingsCache.encrypted) was not being invalidated. This caused getSettingsForConversation() to return stale cached settings instead of fetching fresh settings with the newly activated profile's LLM config. The fix adds a call to SettingsService.invalidateCache() in the useActivateLlmProfile mutation's onSuccess handler, ensuring that conversations started after activating a profile use the active profile's settings rather than whatever profile was last cached. Changes: - Call SettingsService.invalidateCache() in useActivateLlmProfile onSuccess - Add test for useActivateLlmProfile cache invalidation - Add test for getSettingsForConversation cache invalidation behavior - Fix profiles-service tests to expect headers object in getProfile calls - Add test coverage for exposeSecrets parameter in getProfile Co-authored-by: openhands <openhands@all-hands.dev>
When adding a new LLM profile, changing the profile name field would
wipe out all other form values (model, API key, base URL, etc).
Root cause: The `initialValuesOverride` prop was created as an inline
empty object `{}` inside the render function. Every time the profile
name changed, the component re-rendered, creating a new object reference.
This triggered the `initialValues` useMemo to recompute with a different
reference, which triggered the effect that resets all form values.
Fix: Use `React.useMemo` to create a stable empty object reference
(`emptyInitialValues`) that persists across re-renders.
Tests added:
- 'preserves form values when changing profile name'
- 'preserves form values when changing profile name after selecting model'
Co-authored-by: openhands <openhands@all-hands.dev>
Rework the LLM profiles implementation to use the ProfilesClient and types from the OpenHands TypeScript SDK (PR #148) instead of the local axios-based ProfilesService. Changes: - Update @openhands/typescript-client to PR #148 commit (3c78f17) - Add createProfilesClient factory to typescript-client.ts adapter - Re-export SDK profile types (ProfileInfo, ProfileListResponse, etc.) - Rewrite ProfilesService as thin wrapper around SDK's ProfilesClient - Update SaveProfileRequest usage to match SDK LLM type (required model) - Update ActivateProfileResponse to use SDK's llm_applied field - Fix tests to use SDK-compatible types
Replace LlmProfileSummary with SDK's ProfileInfo type directly. No need for the alias since all usages are internal.
- Use SETTINGS_QUERY_KEYS in use-activate-llm-profile - Apply prettier formatting to profile components
- Fix rename-then-save retry bug: Update originalProfileName immediately after successful rename to prevent retry from using non-existent old name - Add meta.disableToast to all mutation hooks to prevent double error toasts: - useSaveLlmProfile - useDeleteLlmProfile - useRenameLlmProfile - useActivateLlmProfile - Use CONFIG_CACHE_OPTIONS in useLlmProfiles for consistent cache settings - Fix misleading test title Co-authored-by: openhands <openhands@all-hands.dev>
|
Fixed critical issues in cf18d4b: Critical fixes:
Other fixes:
Ready for another look. |
all-hands-bot
left a comment
There was a problem hiding this comment.
This PR adds solid LLM profile management, but several critical issues from previous reviews remain unresolved. The cache pollution and race condition bugs are production-breaking, and test coverage for the primary save flows is still missing.
- Add backend identity (backend.id, orgId) to LLM profiles query key to prevent cache pollution when switching between backends - Add race condition fix in handleEditProfile: track latest request and ignore stale responses from concurrent profile edits - Import useActiveBackend in useLlmProfiles hook Co-authored-by: openhands <openhands@all-hands.dev>
|
Addressed additional critical issues in e2e4e36: Critical fixes:
Ready for another look. |
all-hands-bot
left a comment
There was a problem hiding this comment.
This PR adds solid LLM profile management functionality, but several critical issues from previous reviews remain unresolved, and new concurrency bugs have been identified.
Key Blocking Issues:
- 🔴 Race condition with concurrent save operations (NEW)
- 🔴 Zero test coverage for primary save flows (unresolved)
- 🔴 API key status shows global setting instead of profile-specific (unresolved)
- 🔴 Race condition with concurrent profile edits (unresolved from previous reviews)
- 🟠 Multiple accessibility issues in modals and menus (NEW)
Important Note: This PR changes agent behavior (LLM settings loading and application). Per repository guidelines, this requires human review after lightweight evaluation runs to ensure benchmark performance is not affected.
See inline comments for details on new issues discovered.
- Fix DeleteProfileModal to prevent close during pending deletion via Escape/backdrop - Fix ProfileActionsMenu click-outside handler to check for trigger button clicks - Consolidate duplicate invalidation tests into comprehensive single tests - Add query key invalidation assertions to activate profile test Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
- Add profile name to aria-label in profile-row.tsx for screen reader context - Add Enter key handler for rename modal form submission - Add onKeyDown prop support to SettingsInput and ProfileNameInput components - Add tests for Enter key submission behavior Co-authored-by: openhands <openhands@all-hands.dev>
|
Addressed review feedback in 9fa7336:
Test coverage comments (E2E tests, MSW-based integration tests) are acknowledged and better suited for a dedicated follow-up PR. Ready for another look. |
Co-authored-by: openhands <openhands@all-hands.dev>
all-hands-bot
left a comment
There was a problem hiding this comment.
Solid implementation with extensive test coverage. No blocking issues, but flagging for human review due to eval/benchmark risk (changes LLM settings loading). See inline comments for minor improvements.
- Add Tab key handling to close menu per WAI-ARIA practices - Make validation explicit with empty check in rename modal - Return ProfileMutationResponse from mutation hooks for consistency Co-authored-by: openhands <openhands@all-hands.dev>
|
Addressed additional review suggestions in c494052:
Other suggestions (client.close error handling, null-model mode consistency) acknowledged with rationale for deferring. All review threads resolved. Ready for final review. |
|
Breaking this PR into smaller ones, with new fixes as well |
Summary
Fixes APP-1596
This PR integrates the LLM profiles feature into the LLM settings page, allowing users to save, edit, rename, activate, and delete named LLM configurations.
Important: LLM profiles are only available in local agent-server mode. Cloud mode uses the original simple settings form (model selector + API key) since the cloud backend does not support LLM profiles yet.
Changes
New Files
API Service:
src/api/profiles-service/profiles-service.api.ts- API service for profile CRUD operationsComponents:
src/components/features/settings/llm-profiles/llm-profiles-list-view.tsx- Main profile list view componentsrc/components/features/settings/llm-profiles/profile-row.tsx- Individual profile row with model info and context menusrc/components/features/settings/llm-profiles/profile-actions-menu.tsx- Context menu with Edit/Activate/Delete actionssrc/components/features/settings/llm-profiles/profile-name-input.tsx- Input field with validationsrc/components/features/settings/llm-profiles/rename-profile-modal.tsx- Modal for renaming profilessrc/components/features/settings/llm-profiles/delete-profile-modal.tsx- Confirmation modal for deleting profilesHooks:
src/hooks/query/use-llm-profiles.ts- Query hook for listing profilessrc/hooks/mutation/use-save-llm-profile.ts- Mutation hook for saving profilessrc/hooks/mutation/use-delete-llm-profile.ts- Mutation hook for deleting profilessrc/hooks/mutation/use-rename-llm-profile.ts- Mutation hook for renaming profilessrc/hooks/mutation/use-activate-llm-profile.ts- Mutation hook for activating profilesUtilities:
src/utils/derive-profile-name.ts- Utility to derive profile name from model stringModified Files
src/routes/llm-settings.tsx- Complete rewrite with cloud/local mode detection:LlmSettingsCloudView(original simple form with model selector + API key)LlmSettingsLocalView(profile management UI with list and edit views)LlmSettingsScreenswitches between views based onuseActiveBackend().backend.kindsrc/hooks/query/query-keys.ts- AddedLLM_PROFILES_QUERY_KEYCloud vs Local Mode
The
LlmSettingsScreencomponent detects the active backend mode and renders the appropriate view:Cloud Mode (
LlmSettingsCloudView):Local Mode (
LlmSettingsLocalView):How It Works
Viewing Profiles (Local Only): The profiles list shows all saved LLM configurations with their model info, API key status, and which one is currently active.
Adding a Profile (Local Only): Click "Add LLM Profile" → Enter a profile name → Configure your LLM settings (model, API key, base URL) → Save → The profile is created and available for activation.
Editing a Profile (Local Only): Click the ⋮ menu on any profile → Click "Edit" → Modify the settings → Save → The profile is updated (with API key preserved if not changed).
Activating a Profile (Local Only): Click the ⋮ menu on any profile → Click "Activate" → The profile's LLM settings are applied to the current agent settings. New conversations will use this profile's configuration.
Renaming/Deleting Profiles (Local Only): Use the context menu on any profile row.
Bug Fixes
Profile Editing Issues
renameProfileAPI before saving config when the name changesSettingsServiceinternal cache when activating a profile, ensuringgetSettingsForConversation()fetches fresh settingsReact.useMemoreference for the empty initial values object, preventing unnecessary form resets on re-renderAccessibility Improvements
aria-expandedandaria-haspopupattributes to profile menu triggerisRequiredprop to ProfileNameInput with proper HTML validationCache Invalidation
SettingsService.invalidateCache()call in delete profile hook for fresh settings after profile deletionBackend Compatibility
The implementation uses the SDK's
/api/profilesendpoints:GET /api/profiles- List profiles with active_profile indicatorGET /api/profiles/{name}- Get profile details (supportsX-Expose-Secrets: encrypted)POST /api/profiles/{name}- Save/update profileDELETE /api/profiles/{name}- Delete profilePOST /api/profiles/{name}/rename- Rename profilePOST /api/profiles/{name}/activate- Activate profile (applies LLM config to settings)Test Coverage
__tests__/routes/llm-settings.test.tsx- UI integration tests including:__tests__/hooks/mutation/use-activate-llm-profile.test.tsx- Cache invalidation tests__tests__/api/profiles-service.test.ts- API service tests__tests__/api/settings-service.test.ts- Settings cache invalidation tests__tests__/components/settings/llm-profiles/rename-profile-modal.test.tsx- Error handling test for rename modalThis PR was created by an AI agent (OpenHands) on behalf of the user.