Files
2026-08-21 16:00:13 +08:00

173 lines
5.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""配置驱动的 AI 提供商五阶段参数管道。
根据 ProviderConfig.parameter_mappings 在分发前对参数进行
转换、归一化、过滤和注入。第五阶段 HandlerStep 处理跨参数
依赖等复杂场景。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from app.ai_provider.config.models import ModelConfig, ProviderConfig
@dataclass
class PipelineContext:
"""管道的输入上下文。"""
params: dict[str, Any] = field(default_factory=dict)
model_config: ModelConfig | None = None
provider_config: ProviderConfig | None = None
messages: list[dict[str, Any]] | None = None
tools: list[dict[str, Any]] | None = None
@dataclass
class PipelineResult:
"""经过管道处理后的结果。"""
params: dict[str, Any] = field(default_factory=dict)
filtered_params: dict[str, Any] = field(default_factory=dict)
messages: list[dict[str, Any]] | None = None
tools: list[dict[str, Any]] | None = None
class PipelineStep:
"""管道步骤的基类。"""
def apply(self, ctx: PipelineContext, result: PipelineResult) -> None:
raise NotImplementedError
class NormalizeStep(PipelineStep):
"""步骤 1:归一化参数范围(夹紧到 min/max)。"""
def apply(self, ctx: PipelineContext, result: PipelineResult) -> None:
mappings = ctx.provider_config.parameter_mappings if ctx.provider_config else {}
for key, value in ctx.params.items():
mapping = mappings.get(key)
if mapping:
if mapping.min is not None:
value = max(mapping.min, value)
if mapping.max is not None:
value = min(mapping.max, value)
result.params[key] = value
else:
result.params[key] = value
class FilterStep(PipelineStep):
"""步骤 2:过滤模型/提供商不支持的参数。"""
def apply(self, ctx: PipelineContext, result: PipelineResult) -> None:
allowed = (
set(ctx.provider_config.parameter_mappings.keys())
if ctx.provider_config
else set(result.params.keys())
)
filtered = {}
for key in list(result.params.keys()):
if key not in allowed:
filtered[key] = result.params.pop(key)
result.filtered_params = filtered
class TransformStep(PipelineStep):
"""步骤 3:转换参数名称和值为提供商所需格式。"""
def apply(self, ctx: PipelineContext, result: PipelineResult) -> None:
mappings = ctx.provider_config.parameter_mappings if ctx.provider_config else {}
remapped: dict[str, Any] = {}
for key, value in result.params.items():
mapping = mappings.get(key)
if mapping:
provider_key = mapping.provider_param
if mapping.transform == "int":
value = int(value)
elif mapping.transform == "str":
value = str(value)
elif mapping.transform == "multiply_1000":
value = int(value * 1000)
remapped[provider_key] = value
else:
remapped[key] = value
result.params = remapped
class InjectStep(PipelineStep):
"""步骤 4:为缺失参数注入提供商默认值。"""
def apply(self, ctx: PipelineContext, result: PipelineResult) -> None:
mappings = ctx.provider_config.parameter_mappings if ctx.provider_config else {}
for mapping in mappings.values():
provider_key = mapping.provider_param
if provider_key not in result.params and mapping.default is not None:
result.params[provider_key] = mapping.default
class HandlerStep(PipelineStep):
"""步骤 5:执行已注册的参数处理器。
处理 YAML 映射规则无法表达的跨参数依赖和复杂变换:
- Anthropic thinking 参数要求 max_tokens > budget_tokens
- 根据模型能力过滤不支持的参数
- 结构性参数重组
"""
def apply(self, ctx: PipelineContext, result: PipelineResult) -> None:
if not ctx.provider_config:
return
from app.ai_provider.pipeline_handlers import ParameterHandlerRegistry
registry = ParameterHandlerRegistry
# 阶段 A:逐参数处理器(来自 ParameterMapping.handler)
for key, mapping in ctx.provider_config.parameter_mappings.items():
if mapping.handler:
handler = registry.get(mapping.handler)
handler(ctx, result, key) # key is the param key
# 阶段 B:全局处理器(来自 ProviderConfig.parameter_handlers)
for handler_name in ctx.provider_config.parameter_handlers:
handler = registry.get(handler_name)
handler(ctx, result, None) # global handler: no param_key
class ParameterPipeline:
"""配置驱动的五阶段参数管道。"""
def __init__(
self,
steps: list[PipelineStep],
provider_config: ProviderConfig | None = None,
) -> None:
self._steps = steps
self._provider_config = provider_config
def run(self, ctx: PipelineContext) -> PipelineResult:
if ctx.provider_config is None and self._provider_config is not None:
ctx.provider_config = self._provider_config
result = PipelineResult(params=dict(ctx.params))
for step in self._steps:
step.apply(ctx, result)
return result
class PipelineFactory:
"""工厂:从提供商配置构建管道。"""
@staticmethod
def create(provider_config: ProviderConfig) -> ParameterPipeline:
return ParameterPipeline(
provider_config=provider_config,
steps=[
NormalizeStep(),
FilterStep(),
TransformStep(),
InjectStep(),
HandlerStep(),
],
)