mpfaffenberger / mpfaffenberger/code_puppy
model_factory.make_model_settings: settings object constructed up to 3 times with later dict mutations dropped — thinking_level branch downgrades AnthropicModelSettings/ResponsesSettings to base ModelSettings
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 814
- Forks
- 278
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 76
Description
Problem
In code_puppy/model_factory.py::make_model_settings (lines ~120-330), the GLM and Anthropic/GPT-5 branches construct a settings dict, assign model_settings = ..., then keep mutating model_settings_dict afterwards — whether the mutation takes effect depends on subtle dict-recreation ordering:
1. GLM thinking key is silently dropped for GLM models that go down the Anthropic/Copilot/GPT-5 branches
Lines ~175-186:
if \"glm-4.7\" in model_name.lower() or \"glm-5\" in model_name.lower():
...
model_settings_dict[\"thinking\"] = {\"type\": \"enabled\", \"clear_thinking\": clear_thinking}
model_settings: ModelSettings = ModelSettings(**model_settings_dict)
ModelSettings is a TypedDict; \"thinking\" is not one of its keys, so this relies on TypedDict construction being non-validating — fine — but then each subsequent branch (is_copilot, gpt-5, anthropic) re-creates the settings from model_settings_dict, so a GLM model routed through e.g. a custom_openai config gets thinking only if it skips all branches. The flow is impossible to reason about: assignment at line ~188 is overwritten up to three times, and which keys are present depends on branch ordering rather than declared intent.
2. The final thinking_level block re-creates plain ModelSettings, discarding subclass settings
Lines ~318-330:
if model_supports_setting(model_name, \"thinking_level\"):
if \"thinking_enabled\" not in model_settings_dict:
model_settings_dict[\"thinking_enabled\"] = True
...
model_settings = ModelSettings(**model_settings_dict)
If a model supports thinking_level and matched an earlier branch (e.g. a Copilot Claude or GPT-5 Responses model), the carefully built OpenAIResponsesModelSettings/AnthropicModelSettings is replaced by a base ModelSettings — losing the class identity that pydantic-ai uses for provider-specific serialization. Today this mostly bites Gemini-3-style configs that declare thinking_level in supported_settings, but it's a landmine.
3. Function shape
~210 lines, 5 mostly-exclusive model-family branches, two construction passes. Splitting into _settings_for_copilot(), _settings_for_gpt5(), _settings_for_anthropic(), _settings_for_gemini_thinking() with a single final construction point would make the drop/overwrite bugs structurally impossible.
Suggested fix
Build model_settings_dict fully first (all branches mutate the dict only), then construct the settings object exactly once at the end, choosing the class via a small mapping. The thinking_level defaults block should mutate the dict before that single construction, not re-create afterwards.
Filed by Zen Reviewer A (code-puppy-60635a)
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in code_puppy/model_factory.py at make_model_settings, reading the GLM, Copilot, GPT-5, Anthropic, and thinking_level branches together. Trace how model_settings_dict and the provider-specific settings classes are constructed. Done means the dictionary is fully mutated before one final construction, with GLM thinking retained and subclass settings no longer replaced by base ModelSettings.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- ai
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100