feat(llm): switch model profile on user message#2192
feat(llm): switch model profile on user message#2192VascoSch92 wants to merge 6 commits intomainfrom
Conversation
API breakage checks (Griffe)Result: Passed |
Coverage Report •
|
||||||||||||||||||||||||||||||
all-hands-bot
left a comment
There was a problem hiding this comment.
🟢 Good taste - Clean, pragmatic implementation
What works well:
- Simple command syntax that solves a real user need
- Registry caching pattern (checks cache before disk I/O) - previous performance concern resolved ✅
- Clear separation: parse → handle → switch
- Tests verify actual behavior, not mocks
- No breaking changes (additive only)
Linus would say: "This is how you do it. Straightforward data flow, no special cases, solves the actual problem."
LGTM! 🚀
48a1809 to
0e254ea
Compare
all-hands-bot
left a comment
There was a problem hiding this comment.
🟢 Good taste - Clean, pragmatic implementation
What works well:
- Simple command syntax that solves a real user need
- Registry caching pattern (checks cache before disk I/O) - previous performance concern resolved ✅
- Clear separation: parse → handle → switch
- Tests verify actual behavior, not mocks
- No breaking changes (additive only)
Linus would say: "This is how you do it. Straightforward data flow, no special cases, solves the actual problem."
LGTM! 🚀
openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py
Outdated
Show resolved
Hide resolved
openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py
Outdated
Show resolved
Hide resolved
openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py
Outdated
Show resolved
Hide resolved
openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py
Outdated
Show resolved
Hide resolved
enyst
left a comment
There was a problem hiding this comment.
Thank you for this!
I think plugging this behavior in the conversation may be easy enough to do (and undo), so IMHO that is fine.
I think maybe we don’t want to send this information to the LLM, it’s just a toggle for the user, isn’t it?
The most important things was to do a first try to understand better the problem. I believe i can make it better :) I will adress you suggestion ;) |
all-hands-bot
left a comment
There was a problem hiding this comment.
🟡 Acceptable - Solid architecture with a UX gap
What works well:
- Clean separation of concerns (
SwitchModelHandleris standalone and focused) - Registry caching pattern prevents redundant disk I/O (previous concern resolved ✅)
- Tests verify actual behavior with real code paths (no mock spam)
- Opt-in feature with no breaking changes
- Solves a real user need pragmatically
Key issue:
User feedback disappears into logs instead of being shown in the conversation. See inline comments.
Linus would say: "The data flow is clean and the code does what it should, but users are left in the dark. When someone types /model, they expect to see something back. Logger output is not user output."
|
Hey @enyst I’ve made some changes based on your feedback. The goal was to pollute the Additionally, I've addressed your comments and added an example (give it a try, it’s fun!). I also added a flag to enable or disable model switching. Let me know what you think! |
| state. If provided, secrets are encrypted when saving and | ||
| decrypted when loading. If not provided, secrets are redacted | ||
| (lost) on serialization. | ||
| allow_model_switching: Whether to allow switching between persisted |
There was a problem hiding this comment.
I wonder, is this necessary? 🤔
There was a problem hiding this comment.
It is very improbable that someone type /model as a mistake. But I wanted to give the possibility not to have that enable. It is just a flag. I can remove it if wished.
| new_llm = new_llm.model_copy(update={"usage_id": profile_name}) | ||
| self.llm_registry.add(new_llm) | ||
|
|
||
| return agent.model_copy(update={"llm": new_llm}) |
There was a problem hiding this comment.
There's a lot to like about the separation... 🤔
| ], | ||
| ), | ||
| with self._state: | ||
| self._state.agent = self.agent |
There was a problem hiding this comment.
IIRC we do all changes to state under a lock, I think?
There was a problem hiding this comment.
Yes. But I should acquire the lock when I type with self._state right?
from the class ConversationState
def __enter__(self: Self) -> Self:
"""Context manager entry."""
self._lock.acquire()
return selfThere was a problem hiding this comment.
Right. I think I was thinking also of a couple other things: base_state.json changes, and changing the agent because we change the LLM. For your context, when we last considered the "statelessness" of the agent, we came up with a design "rule", or perhaps desire, to have strict, maybe single, pathways in the codebase, where the components of the agent can be modified. Please take a look at this:
I'd love your thoughts!
Thank you! Left a few tiny comments I think maybe it doesn't get saved, so it won't be restored, the initial profile is? As in, restoring a conversation from the saved Just a heads up, I won't be around for a bit, back later (some number of hours) |
So I ran a small experiment to see what is happening. If we save a convo and we restore it later: the last used LLM will be used. Which makes sense, right? |
Ahh. Because we always save/overwrite |
i think yes. More generally, I think having this kind of machinery (command handlers injected into the conversation) could be useful for other features we want to implement. For example, #1929 is a great use case for this. |
Summary
Implementation of the
/model <MODEL_NAME> [prompt]command (ref #2018 )The command functions as follows:
/model: Returns a list of available LLM profiles./model <MODEL_NAME>: Switches the current LLM profile to the one specified./model <MODEL_NAME> <PROMPT>: Switches the LLM profile and immediately sends the provided prompt to the new model.Note:
LocalConversationclass clean.Checklist
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:ce2c80a-pythonRun
All tags pushed for this build
About Multi-Architecture Support
ce2c80a-python) is a multi-arch manifest supporting both amd64 and arm64ce2c80a-python-amd64) are also available if needed