"""参数处理器注册中心与内置 handlers。 处理跨参数依赖、结构性转换等复杂场景——这些无法通过 YAML ParameterMapping 的 min/max/transform/default 表达。 使用方式(在 providers.yaml 中): ```yaml parameter_mappings: thinking: { provider_param: thinking, handler: anthropic_thinking } parameter_handlers: - strip_unsupported_capability_params ``` """ from __future__ import annotations from collections.abc import Callable from app.ai_provider.pipeline import PipelineContext, PipelineResult # Type alias for a parameter handler function. # param_key is passed for per-param handlers (from ParameterMapping.handler); # for global handlers (from ProviderConfig.parameter_handlers), param_key is None. ParameterHandler = Callable[[PipelineContext, PipelineResult, str | None], None] class ParameterHandlerRegistry: """全局参数处理器注册中心。 使用 register() 装饰器注册 handler,名称字符串即可在 YAML 中引用。 """ _handlers: dict[str, ParameterHandler] = {} @classmethod def register(cls, name: str) -> Callable[[ParameterHandler], ParameterHandler]: """装饰器:将函数注册为命名 handler。""" def decorator(handler: ParameterHandler) -> ParameterHandler: cls._handlers[name] = handler return handler return decorator @classmethod def get(cls, name: str) -> ParameterHandler: """按名称获取 handler。未找到时抛出 KeyError。""" if name not in cls._handlers: msg = f"Unknown parameter handler: '{name}'. " msg += f"Available: {', '.join(sorted(cls._handlers))}" raise KeyError(msg) return cls._handlers[name] @classmethod def list(cls) -> list[str]: """返回所有已注册的 handler 名称。""" return list(cls._handlers.keys()) # ── 内建 Handlers ────────────────────────────────────────────── @ParameterHandlerRegistry.register("anthropic_thinking") def handle_anthropic_thinking( _ctx: PipelineContext, result: PipelineResult, _param_key: str | None = None, ) -> None: """处理 Anthropic thinking 参数的跨参数约束。 Anthropic API 要求: - max_tokens 必须大于 thinking.budget_tokens - 开启 thinking 时 temperature 必须为 1(不应传此参数) - thinking 参数自身需转换为 {type, budget_tokens} 结构 """ thinking = result.params.get("thinking") if thinking is None: return # 将标量 thinking=True 转换为启用 extended thinking if thinking is True: result.params["thinking"] = {"type": "enabled"} # 提取 budget_tokens if isinstance(thinking, dict): budget = thinking.get("budget_tokens", 0) # 确保 max_tokens > budget_tokens max_tokens = result.params.get("max_tokens", 0) if budget > 0 and max_tokens <= budget: result.params["max_tokens"] = budget + 1 # 开启 thinking 时移除 temperature 参数(Anthropic 静默忽略, # 但显式移除更安全干净) result.params.pop("temperature", None) @ParameterHandlerRegistry.register("validate_max_tokens") def validate_max_tokens( ctx: PipelineContext, result: PipelineResult, _param_key: str | None = None, ) -> None: """校验 max_tokens 不超过模型上限。""" model_config = ctx.model_config if model_config is None: return max_tokens = result.params.get("max_tokens") if max_tokens is None: return limit = model_config.max_output_tokens if limit and max_tokens > limit: result.params["max_tokens"] = limit @ParameterHandlerRegistry.register("validate_temperature") def validate_temperature( _ctx: PipelineContext, _result: PipelineResult, _param_key: str | None = None, ) -> None: """温度参数跨提供商规范校验。 非 reasoning/thinking 模式下强制温度 1.0 不是必需的规则, OpenAI/DeepSeek 无此要求,此 handler 留作扩展点。 """ # 预留:可为特定 provider 添加温度校验逻辑 pass @ParameterHandlerRegistry.register("strip_unsupported_capability_params") def strip_unsupported_capability_params( ctx: PipelineContext, result: PipelineResult, _param_key: str | None = None, ) -> None: """根据模型能力过滤不支持的参数。 当模型不支持 vision 时,移除 image_url 等视觉参数。 当模型不支持 tool_use 时,移除 tools 相关参数。 """ model_config = ctx.model_config if model_config is None: return caps = model_config.resolved_capabilities() if not caps.get("vision", False): result.params.pop("image_url", None) result.params.pop("image", None) if not caps.get("thinking", False): result.params.pop("thinking", None)