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

154 lines
4.9 KiB
Python

"""参数处理器注册中心与内置 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)