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

@ -812,7 +812,18 @@ def run_doctor(args):
"(should be under 'model:' section)"
)
if should_fix:
model_section = raw_config.setdefault("model", {})
# Coerce scalar/None ``model:`` into a dict before mutation —
# ``setdefault("model", {})`` would return an existing scalar
# and then ``model_section[k] = ...`` would raise TypeError.
raw_model = raw_config.get("model")
if isinstance(raw_model, dict):
model_section = raw_model
elif isinstance(raw_model, str) and raw_model.strip():
model_section = {"default": raw_model.strip()}
raw_config["model"] = model_section
else:
model_section = {}
raw_config["model"] = model_section
for k in stale_root_keys:
if not model_section.get(k):
model_section[k] = raw_config.pop(k)