-
Notifications
You must be signed in to change notification settings - Fork 109
feat(llm): add LLM profiles #795
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…authored-by: openhands <openhands@all-hands.dev>
…sation startup\n\n- ProfileManager manages ~/.openhands/llm-profiles/*.json (load/save/list/register)\n- LocalConversation now calls ProfileManager.register_all to eagerly populate LLMRegistry\n\nCo-authored-by: openhands <openhands@all-hands.dev>
4169ded to
46ca1b7
Compare
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
1b4ece7 to
9cbf67f
Compare
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
- embed profile lifecycle APIs into the registry - update persistence helpers, docs, and examples to use registry - replace profile manager tests with registry profile coverage Co-authored-by: openhands <openhands@all-hands.dev>
- note that LLMRegistry is the unified entry point for disk and runtime profiles - mention how to override the profile directory when embedding the SDK Co-authored-by: openhands <openhands@all-hands.dev>
- rename payload helpers to resolve_llm_profiles/compact_llm_profiles - update conversation state to use clearer helper names - drop the optional agent_settings convenience module and its tests Co-authored-by: openhands <openhands@all-hands.dev>
- replace the _transform flag with dedicated _compact/_resolve helpers - make compact_llm_profiles/resolve_llm_profiles easier to follow by delegating to the new helpers Co-authored-by: openhands <openhands@all-hands.dev>
|
@OpenHands Understand the goal of this PR. Merge main into its branch and fix the conflicts. Please make sure to not lose what main branch did in state! |
|
I'm on it! enyst can track my progress at all-hands.dev |
Co-authored-by: openhands <openhands@all-hands.dev>
Final summary (new since my last summary)Double-checks
What changed (net-new)Nothing new since the last update: the branch is clean and fully synced with the remote ( |
|
@OpenHands 2 tests are failing, an llm/ test and an mcp/ test. Figure it out please, then tell me what the problem was. Tell me in a comment on github. |
|
I'm on it! enyst can track my progress at all-hands.dev |
Co-authored-by: openhands <openhands@all-hands.dev>
|
I tracked down the failing Root cause: Fix: switched Commit: b5a01ad |
Final summary (since last summary)What I did
Root cause
Code changes (concise)
Verification
Checklist against your request/instructions
No additional unpushed diffs remain ( |
This reverts commit b5a01ad.
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
all-hands-bot
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review Summary
I've completed a comprehensive analysis of the PR changes using bash commands and static analysis tools. Here's my structured review:
Code Review: feat(llm): add LLM profiles
Overview
This PR adds LLM profile persistence to the SDK, allowing users to save, load, and manage LLM configurations. The implementation includes comprehensive tests and proper security considerations.
Test Results ✅
All tests pass:
tests/sdk/llm/test_llm_registry_profiles.py: 22/22 passedtests/sdk/conversation/local/test_state_serialization.py: 22/22 passed- Static analysis (ruff): All checks passed
Files Changed
- Core functionality:
llm_registry.py,llm.py,conversation/state.py - Infrastructure:
persistence/__init__.py,persistence/settings.py - Tooling:
check_documented_examples.py - Examples:
34_llm_profiles.py - Tests:
test_llm_registry_profiles.py,test_state_serialization.py
Issues Identified
1. Style and Formatting
✅ No significant style or formatting issues found. Code follows PEP8 and passes ruff checks.
2. Clarity and Readability
[openhands-sdk/openhands/sdk/llm/llm_registry.py, Lines 161-168] 🛠️ Minor TOCTOU consideration
There's a small time-of-check to time-of-use (TOCTOU) window between file creation and permission setting:
with path.open("w", encoding="utf-8") as handle:
json.dump(data, handle, indent=2, ensure_ascii=False)
# Apply restrictive permissions when creating a new file
if not existed_before:
try:
path.chmod(0o600)Impact: Low - the window is microseconds, and default permissions are typically not world-writable.
Suggestion: For maximum security on POSIX systems, consider using os.open() with explicit permissions, though the current implementation is acceptable for most use cases.
[openhands-sdk/openhands/sdk/llm/llm_registry.py, Line 167] 🔍 Broad exception handling
The code uses a bare except Exception for chmod operations. Status: Acceptable - This is documented as best-effort for non-POSIX systems (e.g., Windows), and the error is logged for debugging.
3. Security and Common Bug Patterns
✅ [openhands-sdk/openhands/sdk/llm/llm_registry.py, Lines 106-115] 🔐 Path Traversal Protection
Good practice: The code properly validates profile IDs to prevent path traversal attacks through multiple layers of validation.
✅ [openhands-sdk/openhands/sdk/llm/llm_registry.py, Lines 152-159] 🔐 Secret Handling
Good practice: Secrets can be optionally excluded from saved profiles, providing flexibility for different security requirements.
✅ [openhands-sdk/openhands/sdk/conversation/state.py, Lines 177-179] 🔐 Profile Reference Persistence
Good practice: Conversation state persists profile references (not inline credentials) by default, preventing accidental exposure of secrets.
✅ [.github/scripts/check_documented_examples.py, Line 38] 🛠️ Improved Regex Pattern
Good fix: The updated regex properly handles nested example directories with arbitrary nesting depth.
Summary
Strengths ⭐
- Comprehensive test coverage with 44 tests covering edge cases
- Strong security practices including path traversal protection and secret exclusion
- Clean API design with intuitive methods (list, load, save, validate)
- Good documentation in code, tests, and examples
- Proper error handling with clear error messages
- Backward compatibility maintained with existing code
Minor Improvements (Optional)
- Consider using
os.open()with explicit permissions for new file creation on POSIX systems - The chmod exception handling could be more specific (e.g.,
PermissionError, OSError)
Recommendation
✅ APPROVE - This is high-quality code with excellent test coverage and security practices. The identified issues are minor and don't block approval. The implementation follows SDK conventions and provides a clean, secure API for profile management.
Additional Notes
- The PR correctly fixes the documentation checker to handle nested examples
- Profile ID validation is thorough and prevents common security issues
- The serialization/deserialization flow properly handles profile references
- File permissions are set to 0600 (owner read/write only) for new profile files
Co-authored-by: openhands <openhands@all-hands.dev>
| agent.verify(state.agent, events=state._events) | ||
| persisted_agent = AgentBase.model_validate( | ||
| persisted_agent_payload, | ||
| context={"llm_registry": registry, INLINE_CONTEXT_KEY: False}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm actually not sure if we want to embed the concept of LLMProfile so deep into the serialization layer?
It feels to me that it can just be a high-level concept - could be even higher than LLMRegistry -> it is essentially a manager over a bunch of JSONs, and can list/save/load a bunch of LLM instances?
So most of these could happen on the user side, e.g.,
llm = LLMProfileManager.load("profile name")
OR even simpler, we can consider these methods as @classmethod for the LLM class, e.g.,
@classmethod
def load_profile(cls, profile_name, profile_dir) -> LLM
@classmethod
def list_profile(cls, profile_dir) -> list[str] (LLM profile names)
def save_profile(self, profile_name, profile_dir) -> None
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@OpenHands I love this review comment! Let's think deeply and make these classmethods of LLM.
We have an issue though: currently, if no profile exists or is mentioned, then we serialize the LLM configuration together with the conversation. I think maybe we can just refer a "default" profile there. WDYT?
If so, we do not need INLINE KEY, so let's remove that everywhere. Use a default profile name for a default profile created when no profile exists, and the user creates an LLM in whatever way. Let's call it default.json if none exists; if the user creates a new (unmatching) LLM without profile when "default" exists, let's overwrite it fully with the new one. All the rest of the behavior remains the same, so please investigate and understand the PR diff first, then fix this review comment.
Do tell us on github what you think about the behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm on it! enyst can track my progress at all-hands.dev
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅ Request coverage / checklist
- Investigated the PR behavior around conversation serialization + LLM profiles.
- Removed the “INLINE KEY” (
INLINE_CONTEXT_KEY/inline_llm_persistence) everywhere (code + tests + persistence re-exports). - Implemented the “default profile” behavior:
- If an LLM has no profile, we create/overwrite
default.json(profile iddefault) and persist only a profile reference in conversation state. - If a user later creates another un-profiled LLM, it overwrites
default.jsonfully.
- If an LLM has no profile, we create/overwrite
- Kept the rest of the behavior unchanged (profile refs still load via
llm_registrycontext). - Updated tests accordingly and ensured they pass.
- Ran pre-commit hooks on the touched files.
- Committed changes with the required
Co-authored-by: openhands <openhands@all-hands.dev>. - Pushed to the existing PR branch (
agent-sdk-18-profile-manager).
Conciseness check
Changes are focused on:
- removing the inline-persistence toggle mechanism,
- ensuring there is always a profile reference via
default, - and updating tests to match the new contract.
No unrelated refactors were kept.
What changed (since last summary)
Core behavior
LLMJSON serialization now emits{"profile_id": ...}wheneverprofile_idis set (no inline-mode toggle).ConversationState.create()now ensures the runtime agent’s LLM is profiled before persisting by calling:registry.ensure_default_profile(agent.llm)and using an immutableagent.model_copy(...).
- Added
LLMRegistry.ensure_default_profile()plusDEFAULT_PROFILE_ID = "default"to implementdefault.jsoncreation/overwrite semantics.
Removed
- Deleted the persistence inline flag constant + exports:
openhands.sdk.persistence.INLINE_CONTEXT_KEYinline_llm_persistencecontext usage throughout.
Tests
- Updated LLM/profile tests and conversation serialization tests to:
- pass
llm_registrywhere needed (for profile ref expansion), - isolate
HOMEper test viamonkeypatch, - add coverage for default profile overwrite behavior.
- pass
Verification
uv run pytest tests/sdk/llm/test_llm_registry_profiles.py tests/sdk/conversation/local/test_state_serialization.py→ PASS (45 tests)uv run pre-commit run --files ...(touched files) → PASS
Git / PR update
- Commit:
23cb1592 feat(llm): default profile persistence and drop inline key - Pushed to:
agent-sdk-18-profile-manager(updates PR feat(llm): add LLM profiles #795)
If you want, I can also propose a small follow-up tweak: avoiding writing default.json when persistence_dir is None (i.e., purely in-memory conversations), but I didn’t change that behavior yet since the instruction emphasized “all the rest remains the same.”
Co-authored-by: openhands <openhands@all-hands.dev>
|
Looks like there are a few issues preventing this PR from being merged!
If you'd like me to help, just leave a comment, like Feel free to include any additional details that might help me get this PR into a better state. You can manage your notification settings |
Summary
LLMRegistry, exposing list/load/save/register/validate helpers with configurable profile directoriesTesting
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.12-nodejs22golang: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:0e60e67-pythonRun
All tags pushed for this build
About Multi-Architecture Support
0e60e67-python) is a multi-arch manifest supporting both amd64 and arm640e60e67-python-amd64) are also available if neededDocs PR (documents new examples for check-examples): OpenHands/docs#219