Structural hardening per user request, following the 3rd round of the same bypass class on PR #5313's reasoning_efforts feature: Round 1: plain-ordering regression (provider-config short-circuit ran before the nested-route deny). Round 2: a provider-qualified hint (@custom:<slug>:vertex/gemini-...) left a slug fragment that the deny's prefix-match missed. Both were legitimate, narrowly-targeted fixes, but the pattern — _nested_route_reasoning_denied() requiring the model string to START WITH the route prefix — meant every future wrapper/nesting scheme would need the strip logic to be updated in lockstep, and a missed case fails OPEN (reasoning re-enabled on a route that must never show it), which is the wrong failure mode for a security-adjacent guard. This commit removes that class of bug instead of patching its latest instance: _nested_route_reasoning_denied() now searches for the vertex/gemini- or gemini_cli/gemini- pattern ANYWHERE in the string at a non-alphanumeric boundary, rather than requiring it at position 0. Correctness no longer depends on _strip_provider_hint_for_reasoning() having stripped exactly the right prefix first — any number of opaque wrapper layers (@provider:, a named custom-provider slug, or any nesting scheme not yet invented) can precede the route and the deny still fires. Verified: all historical bypass strings (round 1 and round 2, plus a hypothetical deeper double-wrapped case) now correctly deny; embedded substrings that must NOT match (e.g. 'notvertex/gemini-x') correctly don't, thanks to the boundary lookbehind. Added test_nested_route_deny_is_boundary_based_not_prefix_based locking in the structural invariant directly, independent of any particular wrapper scheme. Verification: ./scripts/test.sh tests/test_reasoning_effort_model_capabilities.py tests/test_custom_provider_bare_model_reasoning.py # 54 passed in 1.31s Full suite: 11257 passed, 64 pre-existing failures (identical file/test set as the prior commit's baseline — network/credential/profile-isolation/fd-leak tests, unrelated to reasoning_efforts).
330 lines
11 KiB
Python
330 lines
11 KiB
Python
"""Tests for model-aware reasoning effort chip visibility."""
|
|
|
|
from api import config as cfg
|
|
|
|
|
|
def test_cursor_acp_models_do_not_support_reasoning_effort_levels():
|
|
assert cfg.resolve_model_reasoning_efforts(
|
|
"cursor/composer-2.5",
|
|
provider_id="cursor-acp",
|
|
) == []
|
|
|
|
|
|
def test_openai_codex_gpt5_supports_reasoning_effort_levels():
|
|
efforts = cfg.resolve_model_reasoning_efforts(
|
|
"gpt-5.5",
|
|
provider_id="openai-codex",
|
|
)
|
|
assert "medium" in efforts
|
|
assert "high" in efforts
|
|
assert "xhigh" in efforts
|
|
assert "max" not in efforts
|
|
|
|
|
|
def test_openai_codex_prefixed_gpt5_supports_reasoning_effort_levels():
|
|
efforts = cfg.resolve_model_reasoning_efforts(
|
|
"@openai-codex:gpt-5.5",
|
|
provider_id="openai-codex",
|
|
)
|
|
assert "medium" in efforts
|
|
assert "high" in efforts
|
|
assert "xhigh" in efforts
|
|
assert "max" not in efforts
|
|
|
|
|
|
def test_openai_codex_max_effort_is_clamped_before_streaming():
|
|
assert cfg.coerce_reasoning_effort_for_model(
|
|
"max",
|
|
"gpt-5.5",
|
|
provider_id="openai-codex",
|
|
) == "xhigh"
|
|
|
|
|
|
def test_unsupported_xhigh_degrades_to_high_not_disabled():
|
|
# o1/o3/o4 on openai-codex cap at low/medium/high. A configured xhigh (or
|
|
# max) must clamp DOWN to the highest supported level (high), not silently
|
|
# disable reasoning by returning "".
|
|
assert cfg.coerce_reasoning_effort_for_model(
|
|
"xhigh",
|
|
"o3-mini",
|
|
provider_id="openai-codex",
|
|
) == "high"
|
|
assert cfg.coerce_reasoning_effort_for_model(
|
|
"max",
|
|
"o3-mini",
|
|
provider_id="openai-codex",
|
|
) == "high"
|
|
|
|
|
|
def test_coerce_never_escalates_above_configured_effort():
|
|
# A supported lower effort is returned verbatim; coercion only degrades.
|
|
assert cfg.coerce_reasoning_effort_for_model(
|
|
"low",
|
|
"gpt-5.5",
|
|
provider_id="openai-codex",
|
|
) == "low"
|
|
|
|
|
|
def test_coerce_preserves_effort_for_unrecognized_model():
|
|
# #3505 review: resolve_model_reasoning_efforts() returns [] for BOTH
|
|
# known-unsupported AND simply-unrecognized models (custom providers,
|
|
# aggregator-rewritten ids, brand-new releases). Coercion must NOT silently
|
|
# drop a configured effort just because we don't recognize the model — that
|
|
# would be a behavior change vs sending it verbatim (master). Preserve the
|
|
# configured level for an empty/unknown capability set; the provider stays
|
|
# the final authority. The known-bad CLAMP paths return a NON-empty set, so
|
|
# they are unaffected (covered by the openai-codex tests above).
|
|
assert cfg.coerce_reasoning_effort_for_model(
|
|
"high",
|
|
"some-unknown-model-xyz",
|
|
provider_id="some-custom-provider",
|
|
) == "high"
|
|
assert cfg.coerce_reasoning_effort_for_model(
|
|
"max",
|
|
"brand-new-model-2099",
|
|
provider_id="some-custom-provider",
|
|
) == "xhigh"
|
|
# 'none' / unset still pass through unchanged for unknown models.
|
|
assert cfg.coerce_reasoning_effort_for_model(
|
|
"none", "some-unknown-model-xyz", provider_id="custom"
|
|
) == "none"
|
|
assert cfg.coerce_reasoning_effort_for_model(
|
|
"", "some-unknown-model-xyz", provider_id="custom"
|
|
) == ""
|
|
|
|
|
|
def test_github_copilot_gpt5_supports_reasoning_effort_levels():
|
|
efforts = cfg.resolve_model_reasoning_efforts(
|
|
"gpt-5.5",
|
|
provider_id="github-copilot",
|
|
)
|
|
assert "medium" in efforts
|
|
assert "high" in efforts
|
|
|
|
|
|
def test_openrouter_anthropic_models_keep_reasoning_effort_levels():
|
|
efforts = cfg.resolve_model_reasoning_efforts(
|
|
"anthropic/claude-sonnet-4.5",
|
|
provider_id="openrouter",
|
|
)
|
|
assert "medium" in efforts
|
|
assert "high" in efforts
|
|
|
|
|
|
def test_non_reasoning_http_models_hide_reasoning_effort_levels():
|
|
assert cfg.resolve_model_reasoning_efforts(
|
|
"meta-llama/llama-3.1-8b-instruct",
|
|
provider_id="openrouter",
|
|
) == []
|
|
|
|
|
|
def test_provider_config_reasoning_efforts_return_filtered_deduped(monkeypatch):
|
|
original = cfg.cfg.get("providers")
|
|
monkeypatch.setitem(
|
|
cfg.cfg,
|
|
"providers",
|
|
{
|
|
"wandb": {
|
|
"reasoning_efforts": [
|
|
" none ",
|
|
"HIGH",
|
|
"bogus",
|
|
"high",
|
|
"xhigh",
|
|
]
|
|
}
|
|
},
|
|
)
|
|
try:
|
|
assert cfg.resolve_model_reasoning_efforts(
|
|
"zai-org/GLM-5.2",
|
|
provider_id="wandb",
|
|
) == ["none", "high", "xhigh"]
|
|
finally:
|
|
if original is None:
|
|
cfg.cfg.pop("providers", None)
|
|
else:
|
|
monkeypatch.setitem(cfg.cfg, "providers", original)
|
|
|
|
|
|
def test_provider_config_all_invalid_falls_through(monkeypatch):
|
|
original = cfg.cfg.get("providers")
|
|
monkeypatch.setitem(
|
|
cfg.cfg,
|
|
"providers",
|
|
{"wandb": {"reasoning_efforts": ["bogus", "typo"]}},
|
|
)
|
|
try:
|
|
result = cfg.resolve_model_reasoning_efforts(
|
|
"zai-org/GLM-5.2",
|
|
provider_id="wandb",
|
|
)
|
|
assert result != []
|
|
assert "bogus" not in result
|
|
assert "typo" not in result
|
|
finally:
|
|
if original is None:
|
|
cfg.cfg.pop("providers", None)
|
|
else:
|
|
monkeypatch.setitem(cfg.cfg, "providers", original)
|
|
|
|
|
|
def test_named_custom_provider_config_reasoning_efforts(monkeypatch):
|
|
original = cfg.cfg.get("custom_providers")
|
|
monkeypatch.setitem(
|
|
cfg.cfg,
|
|
"custom_providers",
|
|
[{"name": "llm-proxy", "reasoning_efforts": ["none", "high", "xhigh"]}],
|
|
)
|
|
try:
|
|
assert cfg.resolve_model_reasoning_efforts(
|
|
"some-model",
|
|
provider_id="custom:llm-proxy",
|
|
) == ["none", "high", "xhigh"]
|
|
finally:
|
|
if original is None:
|
|
cfg.cfg.pop("custom_providers", None)
|
|
else:
|
|
monkeypatch.setitem(cfg.cfg, "custom_providers", original)
|
|
|
|
|
|
def test_acp_guards_win_over_configured_reasoning_efforts(monkeypatch):
|
|
original = cfg.cfg.get("providers")
|
|
monkeypatch.setitem(
|
|
cfg.cfg,
|
|
"providers",
|
|
{"copilot-acp": {"reasoning_efforts": ["high"]}},
|
|
)
|
|
try:
|
|
assert cfg.resolve_model_reasoning_efforts(
|
|
"some-model",
|
|
provider_id="copilot-acp",
|
|
) == []
|
|
finally:
|
|
if original is None:
|
|
cfg.cfg.pop("providers", None)
|
|
else:
|
|
monkeypatch.setitem(cfg.cfg, "providers", original)
|
|
|
|
|
|
def test_nested_route_deny_wins_over_configured_reasoning_efforts(monkeypatch):
|
|
original = cfg.cfg.get("custom_providers")
|
|
monkeypatch.setitem(
|
|
cfg.cfg,
|
|
"custom_providers",
|
|
[{"name": "agg", "reasoning_efforts": ["low", "high"]}],
|
|
)
|
|
try:
|
|
for model in ("vertex/gemini-image-1.0", "vertex/gemini-embedding-001"):
|
|
assert cfg.resolve_model_reasoning_efforts(
|
|
model,
|
|
provider_id="custom:agg",
|
|
) == []
|
|
finally:
|
|
if original is None:
|
|
cfg.cfg.pop("custom_providers", None)
|
|
else:
|
|
monkeypatch.setitem(cfg.cfg, "custom_providers", original)
|
|
|
|
|
|
def test_nested_route_deny_wins_for_provider_qualified_hinted_model(monkeypatch):
|
|
# Regression for the deeper bypass: a provider-qualified hint like
|
|
# "@custom:agg:vertex/gemini-image-1.0" must strip BOTH the "@custom:"
|
|
# wrapper AND the named-provider slug "agg:" before the nested-route
|
|
# deny check runs. A naive first-colon split only strips "@custom:",
|
|
# leaving "agg:vertex/gemini-image-1.0" — which no longer starts with
|
|
# "vertex/gemini-" — so the deny is missed and the configured
|
|
# ["low", "high"] leaks through on an image/embedding route.
|
|
original = cfg.cfg.get("custom_providers")
|
|
monkeypatch.setitem(
|
|
cfg.cfg,
|
|
"custom_providers",
|
|
[{"name": "agg", "reasoning_efforts": ["low", "high"]}],
|
|
)
|
|
try:
|
|
for model in (
|
|
"@custom:agg:vertex/gemini-image-1.0",
|
|
"@custom:agg:vertex/gemini-embedding-001",
|
|
):
|
|
assert cfg.resolve_model_reasoning_efforts(
|
|
model,
|
|
provider_id="custom:agg",
|
|
) == []
|
|
finally:
|
|
if original is None:
|
|
cfg.cfg.pop("custom_providers", None)
|
|
else:
|
|
monkeypatch.setitem(cfg.cfg, "custom_providers", original)
|
|
|
|
|
|
def test_nested_route_deny_is_boundary_based_not_prefix_based():
|
|
# Structural regression test for the underlying invariant, independent
|
|
# of any particular wrapper/strip scheme: _nested_route_reasoning_denied
|
|
# must catch the vertex/gemini- or gemini_cli/gemini- route no matter
|
|
# how many opaque wrapper layers precede it in the raw string, as long
|
|
# as the route starts at a non-alphanumeric boundary. This was bypassed
|
|
# twice via different prefix-stripping edge cases (PR #5313) before the
|
|
# check itself was made boundary-based instead of prefix-based, so no
|
|
# future wrapper scheme can reintroduce the same class of bug.
|
|
denied_cases = [
|
|
"vertex/gemini-image-1.0",
|
|
"vertex/gemini-embedding-001",
|
|
"@custom:agg:vertex/gemini-image-1.0",
|
|
"agg:vertex/gemini-image-1.0", # the literal leftover fragment from the historical bug
|
|
"gemini_cli/gemini-imagine-2",
|
|
"outer:inner:vertex/gemini-image-1.0", # hypothetical deeper future nesting
|
|
"@custom:outer:@custom:inner:vertex/gemini-embedding-001",
|
|
]
|
|
for model in denied_cases:
|
|
assert cfg._nested_route_reasoning_denied(model) is True, model
|
|
|
|
allowed_cases = [
|
|
"vertex/gemini-2.5-pro",
|
|
"notvertex/gemini-image-1.0", # embedded in a larger token — must NOT match
|
|
"somegemini_cli/gemini-image-1", # same — embedded substring, not a boundary
|
|
"",
|
|
]
|
|
for model in allowed_cases:
|
|
assert cfg._nested_route_reasoning_denied(model) is False, model
|
|
|
|
|
|
def test_get_reasoning_status_includes_supported_efforts(monkeypatch):
|
|
monkeypatch.setattr(
|
|
cfg,
|
|
"resolve_model_reasoning_efforts",
|
|
lambda *a, **k: ["low", "medium", "high"],
|
|
)
|
|
status = cfg.get_reasoning_status(
|
|
model_id="gpt-5.5",
|
|
provider_id="openai-codex",
|
|
)
|
|
assert status["supported_efforts"] == ["low", "medium", "high"]
|
|
assert status["supports_reasoning_effort"] is True
|
|
|
|
|
|
def test_get_reasoning_status_for_reasoning_capable_model_has_no_max():
|
|
status = cfg.get_reasoning_status(
|
|
model_id="gpt-5.5",
|
|
provider_id="openai-codex",
|
|
)
|
|
assert status["supported_efforts"] == ["minimal", "low", "medium", "high", "xhigh"]
|
|
assert status["supports_reasoning_effort"] is True
|
|
assert "max" not in status["supported_efforts"]
|
|
|
|
|
|
def test_get_reasoning_status_coerces_stale_max_to_xhigh(monkeypatch):
|
|
"""A previously-saved `agent.reasoning_effort: max` (no longer a valid effort)
|
|
must be reported as the coerced `xhigh`, not the raw stale `max`, so the
|
|
boot/status/chip read paths agree with what streaming actually sends."""
|
|
monkeypatch.setattr(
|
|
cfg,
|
|
"_load_yaml_config_file",
|
|
lambda *a, **k: {"agent": {"reasoning_effort": "max"}},
|
|
)
|
|
status = cfg.get_reasoning_status(
|
|
model_id="gpt-5.5",
|
|
provider_id="openai-codex",
|
|
)
|
|
assert status["reasoning_effort"] == "xhigh"
|
|
assert status["reasoning_effort"] != "max"
|