8275a04903
CI / ai-gates (push) Successful in 38s
新增 app/providers/openai/:client 传输(JSON + multipart)、errors 错误映射、 models 厂商 DTO、capabilities 尺寸算术约束、tasks 同步→异步桥、image 图片业务。 装配与配置同步接入,缺 OPENAI_API_KEY 时跳过装配、服务仍可启动。 与火山视频的三处根本差异及其处置: - 接口同步(无任务 ID、无查询端点)→ SyncTaskRegistry 在本服务内部吸收, 对外仍是 ADR-023 §4 的两段式提交/轮询。代价:本服务从此持有状态,进程重启 丢在途任务,且横向扩容前须先解决任务 ID 只在签发进程内有效的问题。 - 产物是 base64 字节而非 URL → 无过期问题,但字节只在内存里。asset_url 放 data URI 仅为满足领域不变式,绝不能原样写进 media.assets.external_url。 - 图生图走 multipart → 先下载参考图再上传;下载失败与超限按参数错误同步拒绝, 不变成注定失败的后台任务。 两处需要记住的判断: - 尺寸上限「3840x2160」文档未说清是逐边还是按像素。逐边理解会禁掉所有高于 2160 的竖图,而 1024x1536 是文档明示的标准竖图,故必然过严。采用最长边 ≤3840 且总像素 ≤3840×2160,已在 capabilities.py 与取证页标为推断。 - 两份文档都没有错误码表,故 errors.py 以 HTTP 状态码为主、type/code 为辅, 顶部标注「未经文档证实」。429 无线索时按限流而非配额耗尽(猜成配额会把可 重试的限流判死);400 无线索时归服务端错误而非内容被拒(错报会让用户去改 一个其实没问题的提示词)。 一处死锁已修并回归钉住:SyncTaskRegistry 原用 add_done_callback 记录完成时刻, 而快速完成的任务在注册时已 done,回调便同步地在持有非可重入锁的线程里执行。 改为首次被观察到时惰性记录。 测试 166 → 271(+105)。四道门禁逐条读退出码,全绿。 产物转存仍阻塞于存储决策(ADR-023 §7),图片模型因此未播种进 V7 目录。
80 lines
3.3 KiB
Python
80 lines
3.3 KiB
Python
"""装配根:把配置变成可用的适配器。
|
|
|
|
**这是唯一允许同时知道「配置」与「具体供应商」的地方。** 其余模块只依赖抽象:
|
|
适配器通过构造参数拿配置(而非自己读环境变量),上层通过 `ProviderRegistry`
|
|
按 `provider_code` 拿适配器。新增一家供应商只需在此登记一行。
|
|
|
|
`app/providers/base.py`、`registry.py` 与 `app/domain/` 对具体供应商零依赖,
|
|
这一点由 `tests/test_layering.py` 自动守着——靠纪律守不住,靠测试才守得住。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from app.config import Settings
|
|
from app.providers.base import GenerationProvider
|
|
from app.providers.openai.client import OpenAiClient
|
|
from app.providers.openai.image import OpenAiImageProvider
|
|
from app.providers.openai.tasks import SyncTaskRegistry
|
|
from app.providers.registry import ProviderRegistry
|
|
from app.providers.volcengine.client import ArkClient
|
|
from app.providers.volcengine.video import SeedanceVideoProvider
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def build_registry(settings: Settings) -> ProviderRegistry:
|
|
"""按配置装配所有可用的适配器。
|
|
|
|
**缺凭证的供应商被跳过而不是让装配失败**:本地只配了一家凭证时,服务仍应能
|
|
起来并提供其余能力与健康检查。真正的失败点在调用时——那时 `find()` 返回
|
|
None,调用方按「适配器不可用」处理,而不是把一个半死的实例放出去。
|
|
"""
|
|
providers: list[GenerationProvider] = []
|
|
|
|
volcengine = settings.volcengine
|
|
if volcengine.api_key:
|
|
providers.append(
|
|
SeedanceVideoProvider(
|
|
ArkClient(
|
|
api_key=volcengine.api_key,
|
|
base_url=volcengine.base_url,
|
|
timeout_sec=volcengine.timeout_sec,
|
|
),
|
|
default_model=volcengine.video_model,
|
|
task_expiry_sec=volcengine.task_expiry_sec,
|
|
)
|
|
)
|
|
else:
|
|
# 警告而非静默:这是最容易被忽略的配置缺失,而症状(任务永远排队)
|
|
# 与它相隔很远。
|
|
log.warning("ARK_API_KEY 未配置,火山方舟视频适配器未装配")
|
|
|
|
openai = settings.openai
|
|
if openai.api_key:
|
|
providers.append(
|
|
OpenAiImageProvider(
|
|
OpenAiClient(
|
|
api_key=openai.api_key,
|
|
base_url=openai.base_url,
|
|
timeout_sec=openai.timeout_sec,
|
|
organization=openai.organization or None,
|
|
),
|
|
# 同步→异步的任务注册表。每个适配器实例一份:它承载的是「本进程
|
|
# 内哪些同步调用正在跑」,共享一份会让两个适配器互相挤占容量。
|
|
SyncTaskRegistry(
|
|
max_workers=openai.max_concurrent,
|
|
completed_ttl_sec=openai.completed_ttl_sec,
|
|
max_entries=openai.max_inflight,
|
|
),
|
|
default_model=openai.image_model,
|
|
)
|
|
)
|
|
else:
|
|
log.warning("OPENAI_API_KEY 未配置,图片生成适配器未装配")
|
|
|
|
registry = ProviderRegistry(providers)
|
|
log.info("已装配 %d 个生成适配器:%s", len(registry), sorted(registry.codes))
|
|
return registry
|