feat(profiles): add active_profile tracking to profiles API#3173
Conversation
Add support for tracking which LLM profile is currently active:
1. GET /api/profiles now returns active_profile field
- Returns null when no profile has been activated
- Returns the profile name when one is active
2. POST /api/profiles/{name}/activate endpoint
- Loads the named profile's LLM configuration
- Applies it to current agent settings
- Records the profile name as active_profile
Changes:
- Add active_profile field to ProfileListResponse
- Add active_profile to PersistedSettings model
- Add SettingsUpdatePayload support for active_profile
- Update list_profiles to include active_profile from settings
- Add activate_profile endpoint with LLM config application
- Add 8 comprehensive tests for the new functionality
This matches the behavior of the OpenHands main app for frontend
compatibility in displaying which profile is currently in use.
Closes #3172
Python API breakage checks — ✅ PASSEDResult: ✅ PASSED |
REST API breakage checks (OpenAPI) — ✅ PASSEDResult: ✅ PASSED |
When no profiles exist but agent_settings.llm has an API key configured, automatically create a profile named after the model (e.g., 'gpt-4o' or 'claude-3.5-sonnet'). This provides a seamless migration path for users with existing LLM configurations. - Add _model_to_profile_name() to convert model names to valid profile names - Strip provider prefixes (e.g., 'openai/gpt-4o' -> 'gpt-4o') - Sanitize special characters to dashes - Auto-set the new profile as active - Add 8 tests covering auto-creation behavior Co-authored-by: openhands <openhands@all-hands.dev>
When renaming a profile that is currently active, the active_profile setting in PersistedSettings was not being updated, effectively orphaning the active profile reference. - Check if renamed profile matches active_profile - Update active_profile to new name if it matches - Add 2 tests: one for active profile rename, one for inactive rename Co-authored-by: openhands <openhands@all-hands.dev>
all-hands-bot
left a comment
There was a problem hiding this comment.
✅ QA Report: PASS
Active profile tracking works correctly end-to-end. All three key features (auto-creation, activation, and rename tracking) function as described and persist across server restarts.
Does this PR achieve its stated goal?
Yes. This PR successfully adds active_profile tracking to the agent server's profiles API for frontend compatibility. The implementation correctly:
- Returns
active_profileinGET /api/profiles(null when inactive, profile name when active) - Activates profiles via
POST /api/profiles/{name}/activate, updating both LLM settings and the active profile marker - Auto-creates a profile from existing LLM settings when no profiles exist
- Updates
active_profilewhen renaming the active profile - Persists state across server restarts
The frontend can now reliably determine which LLM profile is active without comparing encrypted API keys.
| Phase | Result |
|---|---|
| Environment Setup | ✅ Dependencies installed, server started on port 8765 |
| CI Status | |
| Functional Verification | ✅ All endpoints work as described, persistence confirmed |
Functional Verification
Test 1: Baseline - Empty state
Ran:
curl -s http://localhost:8765/api/profilesResult:
{
"profiles": [],
"active_profile": null
}Interpretation: Confirms default state - no profiles, active_profile is null as expected.
Test 2: Auto-creation from LLM settings
Step 1 — Configure LLM with API key:
curl -X PATCH http://localhost:8765/api/settings \
-H "Content-Type: application/json" \
-d '{
"agent_settings_diff": {
"llm": {
"model": "openai/gpt-4o",
"api_key": "sk-test-auto-create-key",
"temperature": 0.7
}
}
}'Step 2 — Request profiles (triggers auto-creation):
curl -s http://localhost:8765/api/profilesResult:
{
"profiles": [
{
"name": "gpt-4o",
"model": "openai/gpt-4o",
"base_url": null,
"api_key_set": true
}
],
"active_profile": "gpt-4o"
}Interpretation: Auto-creation works correctly. Profile was created with sanitized model name ("openai/gpt-4o" → "gpt-4o"), includes API key, and is immediately marked as active.
Test 3: Activate different profile
Step 1 — Create second profile:
curl -X POST http://localhost:8765/api/profiles/claude-3-opus \
-H "Content-Type: application/json" \
-d '{
"llm": {
"model": "anthropic/claude-3-opus",
"api_key": "sk-ant-test-key",
"temperature": 0.8
}
}'Step 2 — Activate the new profile:
curl -X POST http://localhost:8765/api/profiles/claude-3-opus/activateResult:
{
"name": "claude-3-opus",
"message": "Profile 'claude-3-opus' activated and applied to current settings",
"llm_applied": true
}Step 3 — Verify active_profile changed:
curl -s http://localhost:8765/api/profiles | jq '.active_profile'Result:
"claude-3-opus"
Step 4 — Verify LLM settings were updated:
curl -s http://localhost:8765/api/settings | jq '.agent_settings.llm | {model, temperature}'Result:
{
"model": "anthropic/claude-3-opus",
"temperature": 0.8
}Interpretation: Activation endpoint works correctly. active_profile updated from "gpt-4o" to "claude-3-opus" and LLM settings applied successfully (model and temperature match the profile).
Test 4: Rename active profile
Step 1 — Rename the active profile:
curl -X POST http://localhost:8765/api/profiles/claude-3-opus/rename \
-H "Content-Type: application/json" \
-d '{"new_name": "claude-opus-v1"}'Step 2 — Verify active_profile was updated:
curl -s http://localhost:8765/api/profiles | jq '.active_profile'Result:
"claude-opus-v1"
Interpretation: Renaming the active profile correctly updates the active_profile field. This prevents orphaned references.
Test 5: Persistence across restarts
Step 1 — Stop the server:
pkill -f "python.*openhands.agent_server.*8765"Step 2 — Restart the server:
uv run -m openhands.agent_server --port 8765 --host 0.0.0.0 &Step 3 — Check active_profile after restart:
curl -s http://localhost:8765/api/profiles | jq '{active_profile, profile_count: (.profiles | length)}'Result:
{
"active_profile": "claude-opus-v1",
"profile_count": 2
}Interpretation: active_profile persisted correctly across server restart. Both profiles and the active marker survived the restart, confirming persistence works.
Test 6: Error handling
Ran:
curl -s -X POST http://localhost:8765/api/profiles/nonexistent/activateResult:
{
"detail": "Profile 'nonexistent' not found"
}Interpretation: Proper 404 error handling for non-existent profiles.
Issues Found
None. All functionality works as described in the PR.
Co-authored-by: openhands <openhands@all-hands.dev>
all-hands-bot
left a comment
There was a problem hiding this comment.
LGTM! Solid implementation of active_profile tracking with comprehensive test coverage. The auto-creation UX convenience is well-handled.
| llm = settings.agent_settings.llm | ||
| profile_name = _model_to_profile_name(llm.model or "default") | ||
| try: | ||
| with _store_errors(): | ||
| store.save( | ||
| profile_name, | ||
| llm, | ||
| include_secrets=True, | ||
| cipher=cipher, | ||
| ) | ||
|
|
||
| # Update settings to mark this as active | ||
| def set_active(s: PersistedSettings) -> PersistedSettings: | ||
| s.active_profile = profile_name | ||
| return s | ||
|
|
||
| settings_store.update(set_active) | ||
| active_profile = profile_name | ||
|
|
||
| # Refresh summaries to include the new profile | ||
| summaries = store.list_summaries() | ||
| logger.info( | ||
| f"Auto-created '{profile_name}' profile from existing LLM settings" | ||
| ) | ||
| except Exception as e: | ||
| # Log but don't fail - auto-creation is a convenience feature |
There was a problem hiding this comment.
🟡 Suggestion: Minor edge case - if store.save() succeeds but settings_store.update() fails, the profile will be created but not marked active. On the next call, auto-creation won't retry (since summaries isn't empty), leaving a profile the user didn't explicitly create.
This is rare (both are file I/O so likely fail together) and the impact is minor (user can manually activate or delete), but worth noting for completeness. Consider whether swapping the order (update settings first, then save) makes sense, or if this edge case is acceptable as-is.
all-hands-bot
left a comment
There was a problem hiding this comment.
✅ QA Report: PASS
Verified active_profile tracking functionality by starting the agent server and making real HTTP requests to test the new API endpoints and persistence behavior.
Does this PR achieve its stated goal?
Yes. The PR successfully adds active_profile tracking to the agent server's profiles API for frontend compatibility. I verified by:
- Starting the agent server and confirming
GET /api/profilesnow returns theactive_profilefield - Activating a profile via
POST /api/profiles/{name}/activateand confirming it applies the LLM config to agent settings - Switching between profiles and verifying both
active_profileand the underlying LLM settings update correctly
| Phase | Result |
|---|---|
| Environment Setup | ✅ All dependencies installed, build successful |
| CI Status | ✅ All checks passing (29/29) |
| Functional Verification | ✅ All core functionality working as described |
Functional Verification
Test 1: Verify active_profile field in GET /api/profiles
Ran:
GET /api/profilesResult:
{
"profiles": [...],
"active_profile": null
}Interpretation: The response includes the new active_profile field, initially set to null as expected. The field is present in the API response, confirming the schema change.
Test 2: Activate a profile
Ran:
POST /api/profiles/claude-profile/activateResult:
{
"name": "claude-profile",
"message": "Profile 'claude-profile' activated and applied to current settings",
"llm_applied": true
}Interpretation: The activate endpoint works correctly, returning a success response with llm_applied: true, confirming the profile's LLM configuration was applied.
Test 3: Verify active_profile updates after activation
Ran:
GET /api/profilesResult:
{
"profiles": [...],
"active_profile": "claude-profile"
}Interpretation: After activation, active_profile correctly reflects the activated profile name. This enables frontends to display which profile is currently active.
Test 4: Verify LLM config applied to agent settings
Ran:
GET /api/settingsResult:
{
"agent_settings": {
"llm": {
"model": "claude-3-opus",
...
}
}
}Interpretation: The agent settings' LLM model matches the activated profile's model (claude-3-opus), confirming activation actually applies the profile's configuration, not just updates the tracking field.
Test 5: Switch to a different profile
Step 1 — Initial state:
active_profile is "claude-profile", agent LLM model is "claude-3-opus".
Step 2 — Activate different profile:
POST /api/profiles/my-custom-profile/activateStep 3 — Verify change:
GET /api/profiles # active_profile: "my-custom-profile"
GET /api/settings # agent_settings.llm.model: "gpt-4o"Interpretation: Switching profiles updates both the active_profile field and the underlying agent LLM configuration. This confirms the feature works correctly for profile switching workflows.
Test 6: Error handling for non-existent profile
Ran:
POST /api/profiles/nonexistent-9999/activateResult:
{
"detail": "Profile 'nonexistent-9999' not found"
}Status: 404
Interpretation: Proper error handling is in place, returning 404 with a clear message when attempting to activate a non-existent profile.
Issues Found
None.
QA Summary: This PR delivers exactly what it promises — frontend-compatible active profile tracking. All endpoints work correctly, persistence is functional, and error handling is appropriate. Ready to merge.
…s#3173) Co-authored-by: openhands <openhands@all-hands.dev>
Summary
This PR adds
active_profiletracking to the agent server's profiles API, matching the behavior of the OpenHands main app for frontend compatibility.Fixes AGE-1512
Changes
API Enhancements
GET /api/profilesnow returnsactive_profilefield:nullwhen no profile has been activatedPOST /api/profiles/{name}/activate(new endpoint):agent_settings.llm)Example Response
{ "profiles": [ { "name": "my_profile", "model": "openai/gpt-4o", "base_url": null, "api_key_set": true } ], "active_profile": "my_profile" }Implementation Details
active_profilefield toProfileListResponseactive_profiletoPersistedSettingsmodel with persistence supportSettingsUpdatePayloadsupport foractive_profileupdatesActivateProfileResponsemodel for the activate endpointlist_profilesendpoint to returnactive_profilefrom settingsTesting
Added 8 comprehensive tests for the new functionality:
test_list_profiles_includes_active_profile_null_by_defaulttest_activate_profile_successtest_activate_profile_updates_active_profiletest_activate_profile_applies_llm_configtest_activate_profile_not_foundtest_activate_profile_with_api_keytest_list_profiles_shows_active_after_activationtest_activate_profile_invalid_nameAll 66 profiles router tests pass ✅
All 27 settings router tests pass ✅
Use Case
The agent-canvas frontend needs to show which LLM profile is currently active. Without backend tracking, the frontend has to guess based on comparing current settings with profile configs, which is error-prone (especially with encrypted API keys).
Closes #3172
This PR was created by an AI agent (OpenHands) on behalf of the repository maintainer.
@malhotra5 can click here to continue refining the PR
Agent Server images for this PR
• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server
Variants & Base Images
eclipse-temurin:17-jdknikolaik/python-nodejs:python3.13-nodejs22-slimgolang:1.21-bookwormPull (multi-arch manifest)
# Each variant is a multi-arch manifest supporting both amd64 and arm64 docker pull ghcr.io/openhands/agent-server:3d6174b-pythonRun
All tags pushed for this build
About Multi-Architecture Support
3d6174b-python) is a multi-arch manifest supporting both amd64 and arm643d6174b-python-amd64) are also available if needed