feat: add multi-image vision analysis
This commit is contained in:
+56
-1
@@ -23,6 +23,8 @@ T-301 已实现对外 API 鉴权基线:`apps.api.authentication.ApiKeyAuthenti
|
||||
|
||||
T-302 已实现生成接口基线:`POST /api/v1/generate/title` 与 `POST /api/v1/generate/image` 已接入 API Key 鉴权、别名解析、计费规则、预扣点、Provider 调用、成功确认和失败退点;图片结果当前以本地 `MEDIA_ROOT` 保存并返回 `image_url`,后续可替换为对象存储。T-613 起内部实现已抽为生成核心 service,旧同步接口仍保持原字段、状态码和错误语义。T-614 起新增异步生图任务接口,提交阶段同步审核 prompt 和预扣点,worker 复用同一套已预扣执行 / 成功确认 / 失败退点阶段,不复制第二套资金逻辑。
|
||||
|
||||
T-619 已实现 `POST /api/v1/analyze/images`:使用独立 `vision` 操作和能力别名,同步接收一张或多张有序图片并返回完整文字。该接口复用 T-613 的准备 / 预扣 / 执行 / 成功确认 / 失败退点核心,不改变标题和生图接口。
|
||||
|
||||
T-614 已实现生图异步任务化:`POST /api/v1/generate/image/tasks` 返回 `202` 与公开 UUID `task_id`,`GET /api/v1/generate/image/tasks/{task_id}` 轮询任务状态和结果。任务表 `ImageGenerationTask` 关联 `user`、`api_key` 与已预扣 `CallRecord`,支持 `Idempotency-Key` 去重、payload 冲突 `409 idempotency_conflict`、租约 / 心跳 / reaper 处理僵尸 running 任务并幂等退点。异步结果 URL 由 `MEDIA_PUBLIC_BASE_URL` 或 `PUBLIC_BASE_URL` 生成,不透传上游临时链接。
|
||||
|
||||
T-615 已实现生图接口用量遥测:旧同步 `POST /api/v1/generate/image` 与新异步提交 `POST /api/v1/generate/image/tasks` 都会写 `cmhub.api.generation_usage` 结构化日志,事件名 `generation_route_usage`。调用方可选带 `X-Client-Version` 请求头,便于按客户端版本观察旧同步路迁移进度;该请求头不参与鉴权、计费或幂等判断。日志只记录 route/user/key 前缀/客户端版本/别名/状态/耗时/错误码等白名单字段,不记录 API Key 明文、prompt、图片 base64 或 provider raw。
|
||||
@@ -171,6 +173,45 @@ GET /api/v1/client/releases/latest?platform=windows
|
||||
|
||||
要点:别名必须映射到声明 `text` 能力的模型;映射到图片模型时返回 `model_not_allowed`;别名无计费规则返回 `no_pricing_rule`。T-604 后 prompt 命中本地敏感词时返回 `content_blocked`,不扣点、不写调用记录、不调上游。若传 `image_url`,服务端只会在 prompt 审核通过后下载公网 `http` / `https` 图片,并在扣点前拒绝内网、回环、链路本地、元数据地址、跳转到内网的地址和超过大小上限的响应;被拒绝时返回 `bad_request` 且不预扣点。
|
||||
|
||||
### `POST /api/v1/analyze/images`
|
||||
|
||||
理解一张或多张图片并同步返回文字。请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt": "比较这些商品图,说明款式、材质和细节差异",
|
||||
"model": "vision-standard",
|
||||
"images": [
|
||||
{"image_base64": "data:image/jpeg;base64,..."},
|
||||
{"image_url": "https://example.com/detail.png"}
|
||||
],
|
||||
"parameters": {"temperature": 0.2}
|
||||
}
|
||||
```
|
||||
|
||||
`images` 必填且保持顺序,每项必须且只能提供一个 `image_url` 或 `image_base64`,允许两种来源混合。默认限制由 `VISION_MAX_IMAGES=8`、`VISION_MAX_IMAGE_BYTES=10485760`、`VISION_MAX_TOTAL_BYTES=33554432` 控制;超限、空列表、格式错误或同项双来源返回 `400 bad_request`,不扣点、不调上游。
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "第一张是商品正面图,第二张展示了领口和面料细节。",
|
||||
"alias": "vision-standard",
|
||||
"model_used": "provider-model-for-debugging",
|
||||
"points_cost": 3,
|
||||
"points_balance": 95,
|
||||
"call_id": 12346
|
||||
}
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- `model` 是 `vision` 操作的能力别名;缺省时使用后台配置的默认别名。别名指向的 `AiModel` 与 Provider 必须同时具备 `text` 和 `vision` 能力,否则返回 `model_not_allowed`。
|
||||
- prompt 审核先于图片下载 / 解码;命中敏感词返回 `content_blocked`。每个 URL 复用现有公网地址、逐跳重定向、超时和 SSRF 防护。第一版不包含图片内容审核。
|
||||
- 第一版按 `PricingRule(operation_type=vision, alias, resolution="")` 对一次请求固定扣点,不按图片数量重复扣费。输入校验失败不预扣;上游失败按现有账务流程幂等退点。
|
||||
- Chat Completions / Gemini Provider 按请求顺序发送多张图片;返回 `text` 保留上游完整文字,不执行标题拆分清洗。
|
||||
- 输入图片只在本次同步请求内存中使用,不保存到数据库或 media;`CallRecord` 只保存最多 500 字结果摘要,不保存 base64、provider raw 或完整上游响应。
|
||||
|
||||
### `POST /api/v1/generate/image`
|
||||
|
||||
生成图片(同步等待)。请求:
|
||||
@@ -389,6 +430,16 @@ Authorization: Bearer sk_cmhub_xxx
|
||||
"requires_image": true,
|
||||
"pricing_status": "unpriced",
|
||||
"prices": []
|
||||
},
|
||||
{
|
||||
"alias": "vision-standard",
|
||||
"operation_type": "vision",
|
||||
"capabilities": ["text", "vision"],
|
||||
"requires_image": true,
|
||||
"pricing_status": "priced",
|
||||
"prices": [
|
||||
{"resolution": "default", "points_cost": 3}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -529,7 +580,7 @@ query_and_apply_recharge_payment(order_no: str, query_func) -> RechargeResult
|
||||
resolve_alias(operation_type: str, alias: str | None) -> ResolvedModel
|
||||
```
|
||||
|
||||
T-102 后,别名解析已接入数据库表 `AiModel` / `ModelAlias`:每次调用读取当前 active 记录,缺省 `alias=None` 时取该 `operation_type` 的默认别名;标题要求 `text` 能力,图片要求 `image` 能力。密钥以 `AiModel.api_key_encrypted` 存储,使用 Fernet 解密后进入 `ResolvedModel`。
|
||||
T-102/T-619 后,别名解析已接入数据库表 `AiModel` / `ModelAlias`:每次调用读取当前 active 记录,缺省 `alias=None` 时取该 `operation_type` 的默认别名;标题要求 `text`,图片生成要求 `image`,图片理解要求同时具备 `text + vision`。密钥以 `AiModel.api_key_encrypted` 存储,使用 Fernet 解密后进入 `ResolvedModel`。
|
||||
|
||||
从 `cmbot` 形状导入配置的管理命令:
|
||||
|
||||
@@ -557,12 +608,16 @@ class Provider(Protocol):
|
||||
image_filename: str = "image.png",
|
||||
resolution: str = "1K", aspect_ratio: str = "1:1",
|
||||
parameters: dict | None = None) -> ImageGenerationResult: ...
|
||||
def analyze_images(self, prompt: str, model: ResolvedModel,
|
||||
images: Sequence[MultimodalImage],
|
||||
parameters: dict | None = None) -> TextGenerationResult: ...
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- `ResolvedModel` 来自数据库 AiModel(形状同 `cmbot/config/ai_models.json`,外加 `capabilities`;`api_key` 在库中以 Fernet 加密,使用时解密,不落明文)。
|
||||
- `TextGenerationResult` 包含 `text`、清洗后的 `titles`、`model_used`、`raw`;`ImageGenerationResult` 包含图片 bytes、`model_used`、`raw`。`raw` 仅供本次解析/排障摘要使用,调用记录不得整包保存或打印,避免 base64 大图和上游敏感字段入库;图片落对象存储并返回 URL 属 T-302 之后的 API 编排职责。
|
||||
- `analyze_images()` 接收有序 `MultimodalImage(data, mime_type)` 序列;Chat / Gemini 适配器翻译为各自多模态 payload,并从原始响应提取完整文字。图片生成 / 编辑专用 Provider 必须明确拒绝该操作。
|
||||
- 适配器按 `api_type`(`chat`/`gemini`/`images`/`images_edits`/`auto`)从注册表选取,新增供应商 = 新增一个适配器,不改对外接口。
|
||||
- `parameters` 为供应商特有参数的安全子集;适配器负责白名单过滤并把统一入参翻译成各家上游格式,核心字段不可被 `parameters` 或 `extra_body` 覆盖。
|
||||
- 配置热生效:每次调用读当前 AiModel/ModelAlias,后台改动及时反映(或带缓存失效)。
|
||||
|
||||
Reference in New Issue
Block a user