Promote per-model settings to a PerModelSettings capability - #831
Open
mpfaffenberger wants to merge 3 commits into
Open
Promote per-model settings to a PerModelSettings capability#831mpfaffenberger wants to merge 3 commits into
mpfaffenberger wants to merge 3 commits into
Conversation
The per-model ModelSettings payload (max_tokens budget, GPT-5 reasoning knobs, Anthropic thinking, GLM extra_body riders, Copilot translation, yolo-mode parallel_tool_calls) previously rode in on the Agent(model_settings=...) constructor kwarg at both construction sites. It now travels through the first-class get_model_settings capability seam as PerModelSettings, joining the rest of the agent's behavior in the capabilities=[...] block. Feature parity: - identical merged payload: pydantic-ai layers agent -> capability -> run settings over the model's base; nothing else contributes capability-level settings, so slot movement is a no-op on the wire - identical snapshot timing: get_model_settings is re-extracted per run, so the capability snapshots make_model_settings() output in __post_init__ to preserve the old build-time freeze - builder still computes settings once, shared by both construction passes; sub-agents construct theirs inline Tests: seam contract (snapshot freeze, make_model_settings equivalence), FunctionModel wire-parity against the old kwarg wiring, and spec-constructibility pin. Existing stubs of _builder.make_model_settings retargeted to the capability module.
Review follow-up: moving settings from the agent slot to the capability slot changes Agent.override(model_settings=...) precedence (the override now merges before the capability, so the snapshot wins conflicts) and leaves the built agent's model_settings attribute None. No code_puppy call site uses either seam (no .override callers, no readers of the built agent's model_settings), so the divergence is documented in the module docstring instead of papered over, and two regression tests pin the chosen semantics: - full three-layer merge (model base < capability < per-run settings) proving run-level overrides keep their historical precedence - the override-below-capability divergence, kept visible on purpose Also from review: spec test now drives from_spec end-to-end, and test stubs return ModelSettings() instead of an annotation-invalid None.
Review pass 2: temperature now overlaps between the model's base settings (0.9) and the capability snapshot (0.1), so the capability-beats-base assertion actually detects an ordering regression instead of passing vacuously.
This was referenced Aug 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fourth entry in the capability-conversion series (#828
SteerInjection, #829HistoryCompaction, #830PluginMessageTransform): the per-modelModelSettingspayload — auto-derivedmax_tokensbudget, GPT-5 reasoning effort/verbosity, Anthropic extended-thinking, GLMextra_bodyriders, Copilot API translation, and the yolo-modeparallel_tool_callsgate — previously rode in on theAgent(model_settings=...)constructor kwarg at both construction sites. It now travels through pydantic-ai's first-classget_model_settingscapability seam asPerModelSettingsincode_puppy/agents/_model_settings.py, joining the rest of the agent's behavior in thecapabilities=[...]block.Upstream precedent: pydantic-ai's own
Thinkingcapability (capabilities/thinking.py) is exactly this shape — a@dataclasssubclass returning a staticModelSettingsfromget_model_settings().Feature parity
_layer_model_settings, verified in the installed 2.31.0 source). Nothing else contributes capability-level settings and per-run settings (agent.run(model_settings=...)) still merge last, so the payload the model receives is unchanged everywhere code_puppy builds agents. Proven by aFunctionModelwire-parity test against the old kwarg wiring, plus a full three-layer merge test (model base < capability < per-run).Agent.override(model_settings=...)replaces the agent-slot settings — previously ours, now empty — so the capability snapshot merges after it and wins conflicting keys; the built agent'smodel_settingsattribute also readsNone. No code_puppy call site uses either seam (verified: zero.override(callers, zero readers of the built agent'smodel_settings). The trade-off is documented in the module docstring and pinned by a dedicated regression test so it stays visible rather than accidental.get_model_settingsis re-extracted on every run (for_runre-resolution), which would silently pick up config edits mid-session.__post_init__therefore snapshotsmake_model_settings()once — config changes keep applying on agent rebuild, exactly as before.build_pydantic_agentconstructs a singlePerModelSettingsshared by both construction passes, mirroring the old singlemake_model_settings()call. Sub-agents construct theirs inline (single pass).get_model_settingsis a configuration seam, not a request hook; documented at both call sites.get_serialization_name()default is kept: the only fields are plain data (model_name/max_tokens), no live agent references. The spec path is tested end-to-end viafrom_spec.make_model_settingsitself is untouched and stays exported frommodel_factory— plugins that import it directly are unaffected.Tests
tests/agents/test_model_settings_capability.py: snapshot-freeze contract,make_model_settingsequivalence, FunctionModel wire-parity vs the old kwarg, three-layer merge precedence, the pinnedAgent.overridedivergence, andfrom_specconstruction._builder.make_model_settings; retargeted tocode_puppy.agents._model_settings.make_model_settingsand their stubs now returnModelSettings()instead of an annotation-invalidNone.test_render_version_check_current) is pre-existing on main.Heads-up
Like #828/#829/#830, this grazes the shared
capabilities=[...]blocks in_builder.pyandsubagent_invocation.py— whichever of the four lands last eats a trivial rebase.Do not merge — awaiting human review.