Core SDK: `param_resolver` duplicates the `canonical_preset_key` normalisation inline instead of calling the single source of truth
Summary
config/param_resolver.py re-implements the preset-name normalisation rule inline (twice), even though config/parse_utils.py already defines canonical_preset_key() as the documented single source of truth for that rule. This is a pure DRY / drift-risk cleanup — not a feature change.
Current behaviour
parse_utils.canonical_preset_key() is explicitly the one spelling rule, so validation and resolution cannot disagree:
# praisonaiagents/config/parse_utils.py:198-207
def canonical_preset_key(value: str) -> str:
"""
The single spelling rule for preset names. Validation and resolution must
not disagree about what counts as the same preset, so both go through here.
...
"""
return value.strip().lower().replace("-", "_")validate_preset_string() (construction-time validation) routes through this helper. But the resolution path in param_resolver.py copies the body inline instead of calling it:
# praisonaiagents/config/param_resolver.py:316-324
# ... treating -/_ as interchangeable so the accepted spellings match
# the closed-set guard in validate_preset_string (which uses the same
# normalization). ...
value_norm = value.strip().lower().replace("-", "_")
for preset_key in presets:
if preset_key.strip().lower().replace("-", "_") == value_norm:
return _apply_preset(preset_key, presets, config_class)The in-code comment itself notes the two must stay in sync with validate_preset_string's normalisation — which is precisely what canonical_preset_key() exists to guarantee.
Why it matters
Maintenance / drift risk. The whole point of canonical_preset_key() is that validation and resolution agree on what counts as the same preset. Today the resolution site holds a hand-copied duplicate of that rule, so any future change to the normalisation (e.g. also normalising internal whitespace) must be made in two places or the two silently diverge — a value that passes construction-time validation could then be rejected at resolution time. No import-time or hot-path cost; this is purely structural.
Category
Duplicate
Capability preserved
- Case-insensitive, whitespace-tolerant,
-/_-interchangeable preset matching in_resolve_string()— identical output, since the helper body is character-for-character the same expression. - All existing accepted preset spellings (e.g.
" sliding_window ","sliding-window") continue to resolve exactly as before. - No change to
make_preset_errorbehaviour on an unknown preset.
Proposed approach
Merge the duplicate: call the existing canonical_preset_key() at the two resolution sites. param_resolver.py already imports from .parse_utils, and parse_utils has no config dependencies, so there is no circular-import risk.
Resolution sketch
# Before (param_resolver.py:18-23)
from .parse_utils import (
detect_url_scheme,
is_path_like,
make_preset_error,
merge_config_with_overrides,
)
# ... param_resolver.py:322-324
value_norm = value.strip().lower().replace("-", "_")
for preset_key in presets:
if preset_key.strip().lower().replace("-", "_") == value_norm:
return _apply_preset(preset_key, presets, config_class)
# After (same behaviour, single source of truth)
from .parse_utils import (
canonical_preset_key,
detect_url_scheme,
is_path_like,
make_preset_error,
merge_config_with_overrides,
)
# ...
value_norm = canonical_preset_key(value)
for preset_key in presets:
if canonical_preset_key(preset_key) == value_norm:
return _apply_preset(preset_key, presets, config_class)Severity
Low
Validation
- Traced firsthand:
canonical_preset_key()atparse_utils.py:198-207is documented as the single normalisation rule;param_resolver.py:322,324re-implement its exact body inline. - Confirmed
param_resolver.pyalready imports from.parse_utils(line 18) and thatparse_utilsimports nothing fromconfig— no circular-import risk. - Behaviour-preserving: the replacement expression is identical to the inline one, so no API or behaviour change.
- Not intentional robustness: the duplication is not a guard or defensive copy; the comment shows it is meant to mirror the canonical rule.
Keep unchanged
canonical_preset_key()andvalidate_preset_string()inparse_utils.py— unchanged; they remain the single source of truth.- The URL-scheme, path-as-source, and LLM-model-name resolution branches in
_resolve_string()— untouched. - All
*_PRESETSregistries and_apply_presetsemantics — untouched. - No public API surface changes.
Source: MervinPraison/PraisonAI