Skip to content

Commit 7a18e43

Browse files
author
Mateusz
committed
fix(config): clearer error when model alias replacement starts with ^
Weighted composite routing uses ^ as a branch separator. A leading ^ creates an empty first branch and produced a generic parser message. Validate explicitly and raise ConfigurationError with a dedicated error_code and actionable wording. Add unit tests for leading ^ and whitespace-prefixed ^. Made-with: Cursor
1 parent 0953fbd commit 7a18e43

2 files changed

Lines changed: 69 additions & 0 deletions

File tree

src/core/config/semantic_validation.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,11 @@ def warn_if_alias_references_without_rules(config: AppConfig) -> None:
718718
)
719719

720720

721+
def _replacement_starts_with_weighted_separator(replacement: str) -> bool:
722+
"""True when the first composite branch would be empty (leading '^' after spaces)."""
723+
return bool(replacement.lstrip().startswith("^"))
724+
725+
721726
def validate_model_aliases(config: AppConfig) -> None:
722727
"""Validate model alias patterns and replacement routing strings at startup.
723728
@@ -726,6 +731,8 @@ def validate_model_aliases(config: AppConfig) -> None:
726731
727732
Validates:
728733
- Regex pattern syntax is valid.
734+
- Replacement does not begin with '^' (weighted branch separator), which would
735+
yield an empty first branch and a confusing parser error.
729736
- Replacement string is valid composite routing grammar (|, ^, [weight=N]).
730737
- No raw separator characters in query-param values.
731738
- Explicit backend names reference registered backends.
@@ -790,6 +797,25 @@ def validate_model_aliases(config: AppConfig) -> None:
790797
},
791798
)
792799

800+
if _replacement_starts_with_weighted_separator(replacement):
801+
raise ConfigurationError(
802+
message=(
803+
f"Model alias at index {idx} has invalid replacement: it starts with '^' "
804+
f"(after optional leading whitespace). In composite weighted routing, '^' "
805+
f"separates branches; a leading '^' creates an empty first branch. "
806+
f'Write the first branch first, e.g. "[first]backend:model^..." instead of '
807+
f'"^[first]backend:model^...". '
808+
f"Alias pattern: '{pattern}'. Replacement: '{replacement}'."
809+
),
810+
details={
811+
"error_code": "invalid_alias_replacement_leading_weighted_separator",
812+
"alias_index": idx,
813+
"alias_pattern": pattern,
814+
"replacement": replacement,
815+
"reason": "leading_caret_weighted_separator",
816+
},
817+
)
818+
793819
routing_input = CompositeRoutingInput(
794820
selector=replacement,
795821
surface=RoutingSurface.MAIN,

tests/unit/core/config/test_model_alias_validation.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,49 @@ def test_empty_branch_in_failover_raises(self):
197197
== "invalid_alias_replacement_syntax"
198198
)
199199

200+
def test_leading_weighted_separator_caret_raises_clear_error(self):
201+
"""Leading '^' creates an empty branch; surface a dedicated message."""
202+
config = AppConfig(
203+
model_aliases=[
204+
ModelAliasRule(
205+
pattern="^alias:oss-code-medium$",
206+
replacement=(
207+
"^[first]zai-coding-plan:glm-5.1"
208+
"^[weight=4]qwen-oauth:qwen/coder-model"
209+
"^[weight=2]opencode-go:opencode-go/mimo-v2-pro"
210+
),
211+
),
212+
],
213+
)
214+
with pytest.raises(ConfigurationError) as exc_info:
215+
validate_model_aliases(config)
216+
217+
assert (
218+
exc_info.value.details.get("error_code")
219+
== "invalid_alias_replacement_leading_weighted_separator"
220+
)
221+
assert "starts with '^'" in exc_info.value.message
222+
assert (
223+
exc_info.value.details.get("reason") == "leading_caret_weighted_separator"
224+
)
225+
226+
def test_leading_whitespace_before_caret_raises_same_error(self):
227+
config = AppConfig(
228+
model_aliases=[
229+
ModelAliasRule(
230+
pattern="^x$",
231+
replacement=" ^openai:gpt-4o^anthropic:claude-3-5-sonnet",
232+
),
233+
],
234+
)
235+
with pytest.raises(ConfigurationError) as exc_info:
236+
validate_model_aliases(config)
237+
238+
assert (
239+
exc_info.value.details.get("error_code")
240+
== "invalid_alias_replacement_leading_weighted_separator"
241+
)
242+
200243
def test_unknown_backend_raises(self):
201244
config = AppConfig(
202245
model_aliases=[

0 commit comments

Comments
 (0)