Skip to content

feat(profiles): add active_profile tracking to profiles API#3173

Merged
malhotra5 merged 4 commits into
mainfrom
feat/active-profile-tracking
May 9, 2026
Merged

feat(profiles): add active_profile tracking to profiles API#3173
malhotra5 merged 4 commits into
mainfrom
feat/active-profile-tracking

Conversation

@malhotra5

@malhotra5 malhotra5 commented May 9, 2026

Copy link
Copy Markdown
Member

Summary

This PR adds active_profile tracking to the agent server's profiles API, matching the behavior of the OpenHands main app for frontend compatibility.

Fixes AGE-1512

Changes

API Enhancements

  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 (new endpoint):

    • Loads the named profile's LLM configuration
    • Applies it to the current agent settings (updates agent_settings.llm)
    • Records the profile name as the active profile

Example Response

{
  "profiles": [
    { "name": "my_profile", "model": "openai/gpt-4o", "base_url": null, "api_key_set": true }
  ],
  "active_profile": "my_profile"
}

Implementation Details

  • Added active_profile field to ProfileListResponse
  • Added active_profile to PersistedSettings model with persistence support
  • Added SettingsUpdatePayload support for active_profile updates
  • Added ActivateProfileResponse model for the activate endpoint
  • Updated list_profiles endpoint to return active_profile from settings

Testing

Added 8 comprehensive tests for the new functionality:

  • test_list_profiles_includes_active_profile_null_by_default
  • test_activate_profile_success
  • test_activate_profile_updates_active_profile
  • test_activate_profile_applies_llm_config
  • test_activate_profile_not_found
  • test_activate_profile_with_api_key
  • test_list_profiles_shows_active_after_activation
  • test_activate_profile_invalid_name

All 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

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:3d6174b-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-3d6174b-python \
  ghcr.io/openhands/agent-server:3d6174b-python

All tags pushed for this build

ghcr.io/openhands/agent-server:3d6174b-golang-amd64
ghcr.io/openhands/agent-server:3d6174bc003b1cc0904b7017d269aa6e9861013f-golang-amd64
ghcr.io/openhands/agent-server:feat-active-profile-tracking-golang-amd64
ghcr.io/openhands/agent-server:3d6174b-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:3d6174b-golang-arm64
ghcr.io/openhands/agent-server:3d6174bc003b1cc0904b7017d269aa6e9861013f-golang-arm64
ghcr.io/openhands/agent-server:feat-active-profile-tracking-golang-arm64
ghcr.io/openhands/agent-server:3d6174b-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:3d6174b-java-amd64
ghcr.io/openhands/agent-server:3d6174bc003b1cc0904b7017d269aa6e9861013f-java-amd64
ghcr.io/openhands/agent-server:feat-active-profile-tracking-java-amd64
ghcr.io/openhands/agent-server:3d6174b-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:3d6174b-java-arm64
ghcr.io/openhands/agent-server:3d6174bc003b1cc0904b7017d269aa6e9861013f-java-arm64
ghcr.io/openhands/agent-server:feat-active-profile-tracking-java-arm64
ghcr.io/openhands/agent-server:3d6174b-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:3d6174b-python-amd64
ghcr.io/openhands/agent-server:3d6174bc003b1cc0904b7017d269aa6e9861013f-python-amd64
ghcr.io/openhands/agent-server:feat-active-profile-tracking-python-amd64
ghcr.io/openhands/agent-server:3d6174b-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:3d6174b-python-arm64
ghcr.io/openhands/agent-server:3d6174bc003b1cc0904b7017d269aa6e9861013f-python-arm64
ghcr.io/openhands/agent-server:feat-active-profile-tracking-python-arm64
ghcr.io/openhands/agent-server:3d6174b-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:3d6174b-golang
ghcr.io/openhands/agent-server:3d6174bc003b1cc0904b7017d269aa6e9861013f-golang
ghcr.io/openhands/agent-server:feat-active-profile-tracking-golang
ghcr.io/openhands/agent-server:3d6174b-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:3d6174b-java
ghcr.io/openhands/agent-server:3d6174bc003b1cc0904b7017d269aa6e9861013f-java
ghcr.io/openhands/agent-server:feat-active-profile-tracking-java
ghcr.io/openhands/agent-server:3d6174b-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:3d6174b-python
ghcr.io/openhands/agent-server:3d6174bc003b1cc0904b7017d269aa6e9861013f-python
ghcr.io/openhands/agent-server:feat-active-profile-tracking-python
ghcr.io/openhands/agent-server:3d6174b-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., 3d6174b-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 3d6174b-python-amd64) are also available if needed

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

github-actions Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   profiles_router.py173994%129, 195, 197, 401–406
openhands-agent-server/openhands/agent_server/persistence
   models.py1492285%164, 200, 254, 289–295, 297, 299, 302, 306, 332, 349–350, 381–384, 387
TOTAL262281151156% 

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>
@malhotra5 malhotra5 marked this pull request as ready for review May 9, 2026 05:23

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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_profile in GET /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_profile when 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 ⚠️ 2 checks failing (REST API/OpenAPI, pre-commit) but agent-server-tests pass
Functional Verification ✅ All endpoints work as described, persistence confirmed
Functional Verification

Test 1: Baseline - Empty state

Ran:

curl -s http://localhost:8765/api/profiles

Result:

{
  "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/profiles

Result:

{
  "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/activate

Result:

{
  "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/activate

Result:

{
  "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>
@malhotra5 malhotra5 requested a review from all-hands-bot May 9, 2026 05:40

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Solid implementation of active_profile tracking with comprehensive test coverage. The auto-creation UX convenience is well-handled.

Comment on lines +171 to +196
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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:

  1. Starting the agent server and confirming GET /api/profiles now returns the active_profile field
  2. Activating a profile via POST /api/profiles/{name}/activate and confirming it applies the LLM config to agent settings
  3. Switching between profiles and verifying both active_profile and 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/profiles

Result:

{
  "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/activate

Result:

{
  "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/profiles

Result:

{
  "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/settings

Result:

{
  "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/activate

Step 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/activate

Result:

{
  "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.

@malhotra5 malhotra5 merged commit f017ef8 into main May 9, 2026
38 checks passed
@malhotra5 malhotra5 deleted the feat/active-profile-tracking branch May 9, 2026 06:18
StressTestor pushed a commit to StressTestor/software-agent-sdk that referenced this pull request Jun 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add active_profile tracking to LLM profiles API

3 participants