154 lines
4.9 KiB
Python
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)
|