#5148·PraisonAI

Core SDK: `param_resolver` duplicates the `canonical_preset_key` normalisation inline instead of calling the single source of truth

Author: MervinPraisonCreated Sep 18, 2026Updated Sep 18, 2026
Labelsbugclaude

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:

python
# 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:

python
# 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_error behaviour 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

python
# 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() at parse_utils.py:198-207 is documented as the single normalisation rule; param_resolver.py:322,324 re-implement its exact body inline.
  • Confirmed param_resolver.py already imports from .parse_utils (line 18) and that parse_utils imports nothing from config — 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() and validate_preset_string() in parse_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 *_PRESETS registries and _apply_preset semantics — untouched.
  • No public API surface changes.