Files
hunjing/backend/tests/ai_provider/test_pipeline_handlers.py
T
2026-08-21 16:00:13 +08:00

306 lines
11 KiB
Python

"""Tests for the parameter handler mechanism (Task 4 extension)."""
from __future__ import annotations
import pytest
from app.ai_provider.config.models import ModelConfig, ParameterMapping, ProviderConfig
from app.ai_provider.pipeline import PipelineContext, PipelineFactory
from app.ai_provider.pipeline_handlers import ParameterHandlerRegistry
class TestParameterHandlerRegistry:
"""Test the handler registry itself."""
def test_register_and_get(self):
@ParameterHandlerRegistry.register("test_handler_1")
def my_handler(_ctx, _result, _param_key=None):
pass
handler = ParameterHandlerRegistry.get("test_handler_1")
assert handler is my_handler
def test_get_unknown_handler_raises(self):
with pytest.raises(KeyError, match="Unknown parameter handler"):
ParameterHandlerRegistry.get("nonexistent_handler")
def test_list_handlers(self):
handlers = ParameterHandlerRegistry.list()
assert "anthropic_thinking" in handlers
assert "validate_max_tokens" in handlers
assert "strip_unsupported_capability_params" in handlers
class TestAnthropicThinkingHandler:
"""Test the anthropic_thinking handler via the pipeline."""
def test_thinking_true_converts_to_dict(self):
"""标量 thinking=True → {type: enabled}"""
provider = ProviderConfig(
name="anthropic",
format="anthropic",
base_url_env="ANTHROPIC_BASE_URL",
api_key_env="ANTHROPIC_API_KEY",
parameter_mappings={
"thinking": ParameterMapping(
provider_param="thinking",
handler="anthropic_thinking",
),
"max_tokens": ParameterMapping(provider_param="max_tokens"),
},
)
pipeline = PipelineFactory.create(provider)
ctx = PipelineContext(params={"thinking": True, "max_tokens": 4096})
result = pipeline.run(ctx)
assert result.params["thinking"] == {"type": "enabled"}
assert result.params["max_tokens"] == 4096
def test_thinking_budget_enforces_max_tokens(self):
"""thinking.budget_tokens 确保 max_tokens > budget_tokens"""
provider = ProviderConfig(
name="anthropic",
format="anthropic",
base_url_env="X",
api_key_env="Y",
parameter_mappings={
"thinking": ParameterMapping(
provider_param="thinking",
handler="anthropic_thinking",
),
"max_tokens": ParameterMapping(provider_param="max_tokens"),
},
)
pipeline = PipelineFactory.create(provider)
# budget=8192, max_tokens=4096 → 不满足,应被抬到 8193
ctx = PipelineContext(params={"thinking": {"budget_tokens": 8192}, "max_tokens": 4096})
result = pipeline.run(ctx)
assert result.params["max_tokens"] >= 8193
def test_thinking_removes_temperature(self):
"""开启 thinking 时 temperature 被移除"""
provider = ProviderConfig(
name="anthropic",
format="anthropic",
base_url_env="X",
api_key_env="Y",
parameter_mappings={
"thinking": ParameterMapping(
provider_param="thinking",
handler="anthropic_thinking",
),
"temperature": ParameterMapping(
provider_param="temperature",
min=0,
max=1,
),
"max_tokens": ParameterMapping(provider_param="max_tokens"),
},
)
pipeline = PipelineFactory.create(provider)
ctx = PipelineContext(params={"thinking": True, "temperature": 0.7, "max_tokens": 4096})
result = pipeline.run(ctx)
assert "temperature" not in result.params
class TestValidateMaxTokensHandler:
"""Test the validate_max_tokens handler."""
def test_clamps_to_model_limit(self):
"""max_tokens 应被裁剪到模型上限"""
model = ModelConfig(
name="test-model",
provider="anthropic",
model="claude-sonnet-4-6",
format="anthropic",
max_output_tokens=16000,
)
provider = ProviderConfig(
name="anthropic",
format="anthropic",
base_url_env="X",
api_key_env="Y",
parameter_mappings={
"max_tokens": ParameterMapping(provider_param="max_tokens"),
},
parameter_handlers=["validate_max_tokens"],
)
pipeline = PipelineFactory.create(provider)
ctx = PipelineContext(
params={"max_tokens": 999999},
model_config=model,
provider_config=provider,
)
result = pipeline.run(ctx)
assert result.params["max_tokens"] == 16000
def test_within_limit_unchanged(self):
"""max_tokens 在合理范围内时不变"""
model = ModelConfig(
name="test-model",
provider="anthropic",
model="claude-sonnet-4-6",
format="anthropic",
max_output_tokens=16000,
)
provider = ProviderConfig(
name="anthropic",
format="anthropic",
base_url_env="X",
api_key_env="Y",
parameter_mappings={
"max_tokens": ParameterMapping(provider_param="max_tokens"),
},
parameter_handlers=["validate_max_tokens"],
)
pipeline = PipelineFactory.create(provider)
ctx = PipelineContext(
params={"max_tokens": 8000},
model_config=model,
provider_config=provider,
)
result = pipeline.run(ctx)
assert result.params["max_tokens"] == 8000
class TestStripUnsupportedCapabilityParams:
"""Test the strip_unsupported_capability_params handler."""
def test_strips_image_params_when_no_vision(self):
"""模型不支持 vision 时移除 image_url"""
model = ModelConfig(
name="test-model",
provider="openai",
model="gpt-4o",
format="openai",
supports_vision=False,
)
provider = ProviderConfig(
name="openai",
format="openai",
base_url_env="X",
api_key_env="Y",
parameter_mappings={
"max_tokens": ParameterMapping(provider_param="max_tokens"),
"image_url": ParameterMapping(provider_param="image_url"),
},
parameter_handlers=["strip_unsupported_capability_params"],
)
pipeline = PipelineFactory.create(provider)
ctx = PipelineContext(
params={"max_tokens": 100, "image_url": "http://example.com/img.png"},
model_config=model,
provider_config=provider,
)
result = pipeline.run(ctx)
assert "image_url" not in result.params
assert "max_tokens" in result.params
def test_keeps_image_params_when_vision_supported(self):
"""模型支持 vision 时保留 image_url"""
model = ModelConfig(
name="test-model",
provider="openai",
model="gpt-4o",
format="openai",
supports_vision=True,
)
provider = ProviderConfig(
name="openai",
format="openai",
base_url_env="X",
api_key_env="Y",
parameter_mappings={
"max_tokens": ParameterMapping(provider_param="max_tokens"),
"image_url": ParameterMapping(provider_param="image_url"),
},
parameter_handlers=["strip_unsupported_capability_params"],
)
pipeline = PipelineFactory.create(provider)
ctx = PipelineContext(
params={"max_tokens": 100, "image_url": "http://example.com/img.png"},
model_config=model,
provider_config=provider,
)
result = pipeline.run(ctx)
assert "image_url" in result.params
def test_strips_thinking_when_not_supported(self):
"""模型不支持 thinking 时移除 thinking 参数"""
model = ModelConfig(
name="test-model",
provider="openai",
model="gpt-4o",
format="openai",
supports_thinking=False,
)
provider = ProviderConfig(
name="openai",
format="openai",
base_url_env="X",
api_key_env="Y",
parameter_mappings={
"thinking": ParameterMapping(provider_param="thinking"),
},
parameter_handlers=["strip_unsupported_capability_params"],
)
pipeline = PipelineFactory.create(provider)
ctx = PipelineContext(
params={"thinking": True},
model_config=model,
provider_config=provider,
)
result = pipeline.run(ctx)
assert "thinking" not in result.params
class TestHandlerIntegration:
"""Test full pipeline integration with handlers."""
def test_anthropic_pipeline_end_to_end(self):
"""模拟完整的 Anthropic pipeline 流程"""
provider = ProviderConfig(
name="anthropic",
format="anthropic",
base_url_env="ANTHROPIC_BASE_URL",
api_key_env="ANTHROPIC_API_KEY",
parameter_mappings={
"max_tokens": ParameterMapping(provider_param="max_tokens", default=4096),
"thinking": ParameterMapping(
provider_param="thinking", handler="anthropic_thinking"
),
"temperature": ParameterMapping(
provider_param="temperature", min=0, max=1, default=1.0
),
},
parameter_handlers=[
"strip_unsupported_capability_params",
"validate_max_tokens",
],
)
model = ModelConfig(
name="claude-sonnet",
provider="anthropic",
model="claude-sonnet-4-6",
format="anthropic",
supports_vision=True,
supports_thinking=True,
max_output_tokens=16000,
)
pipeline = PipelineFactory.create(provider)
ctx = PipelineContext(
params={
"thinking": {"budget_tokens": 8000},
"temperature": 0.7,
},
model_config=model,
provider_config=provider,
)
result = pipeline.run(ctx)
# thinking → provider_param name
assert "thinking" in result.params
# max_tokens injected via default, then lifted by handler
assert result.params["max_tokens"] >= 8001
# temperature removed by handler
assert "temperature" not in result.params