BUG: Model.to_dict() drops custom_role_conversions and client_kwargs connection settings (api_base/organization/project/azure_endpoint), so a saved agent reloads against the wrong endpoint and role mapping
Problem
Model.to_dict() (src/smolagents/models.py:596) does not export custom_role_conversions, and it does not export the connection settings (api_base, organization, project, azure_endpoint) for the models that hold them inside self.client_kwargs. MultiStepAgent.to_dict() embeds this dict under "model" (src/smolagents/agents.py:997) and save() writes it to agent.json (src/smolagents/agents.py:940), while MultiStepAgent.from_dict() rebuilds the model from it via model_class.from_dict(model_info["data"]) (src/smolagents/agents.py:1029). A saved agent therefore reloads with default role conversions and no custom endpoint, silently pointing back at api.openai.com (or the provider default) instead of the user's endpoint.
Root cause, two independent omissions in the attribute loop at src/smolagents/models.py:604-616:
- The loop asks for the attribute name
"custom_role_conversion"(singular,models.py:605), butApiModel.__init__stores the value asself.custom_role_conversions(plural,models.py:1171).hasattr()is alwaysFalse, so the value is never exported. OpenAIModel.__init__putsapi_base/organization/projectonly intoself.client_kwargs(asbase_url/organization/project,models.py:1683-1689), andAzureOpenAIModel.__init__putsazure_endpoint/api_base/organizationonly intoself.client_kwargs(models.py:1830-1836). None of these become attributes, sohasattr(self, "api_base")etc. isFalseand they are dropped as well.
Steps to reproduce
from smolagents.models import LiteLLMModel, OpenAIModel
m = LiteLLMModel(model_id="gpt-4o-mini", custom_role_conversions={"tool-call": "user"})
print("LiteLLMModel live :", m.custom_role_conversions)
d = m.to_dict()
print("LiteLLMModel dict :", d)
print("after round-trip :", LiteLLMModel.from_dict(d).custom_role_conversions)
o = OpenAIModel(model_id="Qwen2.5-7B", api_base="http://localhost:8000/v1", api_key="dummy", organization="org1")
print("OpenAIModel client_kwargs:", o.client_kwargs)
print("OpenAIModel.to_dict() :", o.to_dict())
Actual behavior and error logs
LiteLLMModel live : {'tool-call': 'user'}
LiteLLMModel dict : {'model_id': 'gpt-4o-mini', 'api_base': None}
after round-trip : {}
OpenAIModel client_kwargs: {'api_key': 'dummy', 'base_url': 'http://localhost:8000/v1', 'organization': 'org1', 'project': None}
OpenAIModel.to_dict() : {'model_id': 'Qwen2.5-7B'}
A second independent reproduction at the attribute level and at the agent level:
live attr : {'tool-call': 'user'}
hasattr sing : False
to_dict() : {'model_id': 'gpt-4o-mini', 'api_base': None}
round-trip attr : {}
agent.json model : {"class": "LiteLLMModel", "data": {"model_id": "gpt-4o-mini", "api_base": "http://localhost:11434/v1"}}
reload roleconv : {} (api_base survived for LiteLLM)
OpenAI to_dict : {'model_id': 'Qwen2.5-7B'} -> reload base_url: None, roleconv: {}
Agent-level round trip: agent.json model block is {"class": "LiteLLMModel", "data": {"model_id": "gpt-4o-mini", "api_base": "http://localhost:8000/v1"}} / RELOADED crc={} / ROUNDTRIP_PRESERVED: False.
For LiteLLMModel, an api_base passed through **kwargs happens to survive because to_dict() spreads self.kwargs, but custom_role_conversions is dropped in every case. For OpenAIModel/AzureOpenAIModel, the endpoint and organization/project/azure_endpoint are dropped unconditionally.
Expected behavior
Model.to_dict() should export the fields its own attribute list names, and from_dict() (the documented inverse, models.py:629-630, which reconstructs via cls(**model_dictionary)) should restore them.
Basis:
- The loop's own attribute list (
models.py:604-616) names exactlycustom_role_conversion,api_base,organization,project,azure_endpoint— they are intended to be exported. ApiModel.__init__stores the mapping asself.custom_role_conversions(models.py:1171), and every adapter exposes the parameter ascustom_role_conversions, so the exported key must match that spelling forcls(**dict)to accept it.- Every concrete
__init__acceptscustom_role_conversions,api_base,organization,project, andazure_endpointas parameters, so restoring them throughfrom_dict()is well-defined.
Environment:
- OS: macOS (Darwin 25.6.0, arm64)
- Python version: 3.12
- Package version: smolagents 1.27.0.dev0, commit
30bb1161095dbae2271e6bc3cc4c219cc3897a57 pip show smolagents:Version: 1.27.0.dev0
Additional context (optional)
Related work found during duplicate checks — none of these cover Model.to_dict() in src/smolagents/models.py:
- #2615 / #2616 / #2714 address agent-level serialization in
src/smolagents/agents.py(agent settings such asinstructions,stream_outputs,code_block_tags). They do not changeModel.to_dict()and do not touchcustom_role_conversionsor theclient_kwargs-held connection settings. - #2301 / #2302 deliberately strip
api_key/tokenfrom serialization, an unrelated intent; this report is about non-secret configuration only. - #532 (closed) was about
custom_role_conversionsnot being applied at generation time, not about serialization.
I am happy to open a PR with the fix once a maintainer labels this status:accepted: correct the key to "custom_role_conversions", and read the connection settings from self.client_kwargs when the attribute is absent (mapping api_base to the client's base_url), keeping api_key/token excluded.
AI disclosure: this issue was prepared with the help of an AI assistant; I have read the code, reproduced the behaviour on the commit above, and stand behind the report.
Checklist
- I have searched the existing issues and have not found a similar bug report.
- I have provided a minimal, reproducible example.
- I have provided the full traceback of the error.
- I have provided my environment details.
- I am willing to work on this issue and submit a pull request. (optional)
Source: huggingface/smolagents