fix(gateway): coerce scalar model: to dict before /model --global persist (#32272)

Reported via AskClaw. When config.yaml has `model: <name>` (flat string)
instead of the nested `model: {default: ..., provider: ...}` form, every
gateway `/model X --global` crashed silently with

    TypeError: 'str' object does not support item assignment

The persist block did:

    model_cfg = cfg.setdefault("model", {})
    model_cfg["default"] = result.new_model

`setdefault` returns the existing scalar, and the next assignment blows
up. The 'switch failed' warning was logged at WARNING level and the user
never saw why their persist didn't stick.

Coerce scalar/None `model:` into a dict before mutation, in both the
gateway path (`gateway/run.py`) and the sister site in
`hermes_cli/doctor.py --fix` (same setdefault-on-string flaw). The CLI
`/model` path is unaffected because it goes through `_set_nested` which
already replaces scalar leaves with dicts.

Regression test `tests/gateway/test_model_command_flat_string_config.py`
covers the flat-string, missing, and proper-dict cases. Without the fix,
the flat-string case fails with the exact original TypeError.
This commit is contained in:
Teknium 2026-05-25 15:22:23 -07:00 committed by GitHub
parent de76f4dbcf
commit 2c6bbaf352
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 185 additions and 2 deletions

View file

@ -10436,7 +10436,21 @@ class GatewayRunner:
cfg = yaml.safe_load(f) or {}
else:
cfg = {}
model_cfg = cfg.setdefault("model", {})
# Coerce scalar/None ``model:`` into a dict before mutation —
# otherwise ``cfg.setdefault("model", {})`` returns the existing
# scalar and the next assignment raises
# ``TypeError: 'str' object does not support item assignment``.
# Reproduces when ``config.yaml`` has ``model: <name>`` (flat
# string) instead of the proper nested ``model: {default: ...}``.
raw_model = cfg.get("model")
if isinstance(raw_model, dict):
model_cfg = raw_model
elif isinstance(raw_model, str) and raw_model.strip():
model_cfg = {"default": raw_model.strip()}
cfg["model"] = model_cfg
else:
model_cfg = {}
cfg["model"] = model_cfg
model_cfg["default"] = result.new_model
model_cfg["provider"] = result.target_provider
if result.base_url: