"""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