#2799·smolagents

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

Author: BlueX888Created Sep 15, 2026Updated Sep 16, 2026

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:

  1. The loop asks for the attribute name "custom_role_conversion" (singular, models.py:605), but ApiModel.__init__ stores the value as self.custom_role_conversions (plural, models.py:1171). hasattr() is always False, so the value is never exported.
  2. OpenAIModel.__init__ puts api_base/organization/project only into self.client_kwargs (as base_url/organization/project, models.py:1683-1689), and AzureOpenAIModel.__init__ puts azure_endpoint/api_base/organization only into self.client_kwargs (models.py:1830-1836). None of these become attributes, so hasattr(self, "api_base") etc. is False and 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 exactly custom_role_conversion, api_base, organization, project, azure_endpoint — they are intended to be exported.
  • ApiModel.__init__ stores the mapping as self.custom_role_conversions (models.py:1171), and every adapter exposes the parameter as custom_role_conversions, so the exported key must match that spelling for cls(**dict) to accept it.
  • Every concrete __init__ accepts custom_role_conversions, api_base, organization, project, and azure_endpoint as parameters, so restoring them through from_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 as instructions, stream_outputs, code_block_tags). They do not change Model.to_dict() and do not touch custom_role_conversions or the client_kwargs-held connection settings.
  • #2301 / #2302 deliberately strip api_key/token from serialization, an unrelated intent; this report is about non-secret configuration only.
  • #532 (closed) was about custom_role_conversions not 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)