Skip to content

smolagent_model_id is annotated str but defaults to a list; the coercion is duplicated at eight sinks #174

Description

@lfnothias

Observation

config.py:64 declares self.smolagent_model_id: str and assigns it a list of four model identifiers. The annotation and the value disagree, and every consumer has had to learn that the field may be either.

The consequence is a coercion repeated at roughly eight sinks: orchestrator.py:287, workflow_factory.py:322, single_agent_factory.py:56, precheck.py:372 and :421, config.py:203 and :219, pricing.py:275, evaluation_cli.py:676. Each independently rediscovers x[0] if isinstance(x, list) else x. Commit 91ef227 ("handle list smolagent_model_id at single-model sinks") fixed one instance of the pattern after it caused a pricing crash.

Why it matters

This is not currently a live bug. I checked the sinks: every functional one is guarded, and the only unguarded ones are display-only (onboard_cli.py:1113 passes the list where a string is expected; evaluation_cli.py:231 compares a string against the list, so no default is ever highlighted). Both are cosmetic.

The cost is that the invariant is duplicated rather than established. Each new sink must remember the coercion, and the one that forgets is a crash — which is exactly how 91ef227 came to be written. A widened type should be normalised once, not defended eight times.

Pathway

  1. Decide the field's real type. The natural one is list[str]: a candidate list with the first entry as the default. Rename to make that explicit if it helps (smolagent_model_ids), or keep the name and fix the annotation.
  2. Normalise at load: in Config.load/__init__, coerce a bare string to a one-element list once, so the rest of the codebase sees a single type.
  3. Expose the default explicitly — a small default_smolagent_model property returning self.smolagent_model_id[0] — and replace the eight ad-hoc coercions with it.
  4. Fix the two display sinks while removing their neighbours.

The change is mechanical and can ship in one commit, since step 2 makes steps 3 and 4 pure deletions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions