feat(ai): 图像主力从 tokenssr 切到 yunqi(支持 images/edits 图生图)

TOKENSSR 余额将尽,切换图像默认 provider 到 yunqi。yunqi 的图生图
端点需 Azure 风格 ?api-version=2025-04-01-preview,不带会 404。

- provider 加 api_version 字段,OpenAICompatible 生图/改图端点自动附加
  ?api-version=(配了才加,tokenssr 等零影响)
- build_provider 解析 api_version:DB(ModelProvider.metadata)优先 → .env 回退
- 迁移 0007:启用 yunqi:gpt-image-2 为唯一 active 图像模型,禁用其余
- 摘除死代码 YunqiProvider 导出(旧类写死"不支持图生图"会误导)

端到端验证:图生图(三视图,36s)+文生图(14s)均正常出图。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-06-18 18:15:34 +08:00
co-authored by Claude Opus 4.8
parent a71be19500
commit 4399da2090
6 changed files with 89 additions and 5 deletions
+5
View File
@@ -204,6 +204,11 @@ PROVIDER_KEYS = {
"yunqi": env("YUNQI_API_KEY", ""),
"tokenssr": env("TOKENSSR_API_KEY", ""),
}
# 中转站生图/改图端点的 Azure 风格 api-version(yunqi 的 images/edits 必须带,否则 404)。
# DB 的 ModelProvider.metadata.api_version 优先,留空回退到这里。
PROVIDER_API_VERSIONS = {
"yunqi": env("YUNQI_API_VERSION", "2025-04-01-preview"),
}
# 火山引擎人像素材库审核(真人资产绿/红盾)· AK/SK 暂借 AirDrama 已邀测账号,张业昌待换成 AirShelf 自有
ASSETS_API = {
@@ -0,0 +1,66 @@
"""图像主力从 tokenssr:gpt-image-2 切到 yunqi:gpt-image-2(TOKENSSR 余额将尽)。
yunqi 的图生图(images/edits)需 Azure 风格 ?api-version=2025-04-01-preview —— 存进
ModelProvider.metadata.api_version,运行时由 services.build_provider 注入,provider 据此拼 URL。
凭证仍走 .env(YUNQI_API_KEY,即能走 edits 的老 key),不进库。
幂等:get_or_create / update_or_create,可重复 apply。
"""
from django.db import migrations
YUNQI_BASE_URL = "https://www.yunqiai.chat/v1"
API_VERSION = "2025-04-01-preview"
def switch(apps, schema_editor):
ModelProvider = apps.get_model("ai", "ModelProvider")
ModelConfig = apps.get_model("ai", "ModelConfig")
yunqi, _ = ModelProvider.objects.get_or_create(
name="yunqi",
defaults={
"display_name": "YunQi AI(New API 网关)",
"status": "active",
"base_url": YUNQI_BASE_URL,
"api_key": "", # 留空:运行时从 .env(YUNQI_API_KEY)解析,密钥不进库
"metadata": {"kind": "relay", "api_version": API_VERSION},
},
)
meta = yunqi.metadata or {}
meta["api_version"] = API_VERSION # edits 端点必带,否则 404
yunqi.metadata = meta
if not yunqi.base_url:
yunqi.base_url = YUNQI_BASE_URL
yunqi.status = "active" # get_default_model 要求 provider__status=active
yunqi.save(update_fields=["metadata", "base_url", "status"])
ModelConfig.objects.update_or_create(
provider=yunqi,
name="gpt-image-2",
capability="image",
defaults={
"display_name": "GPT-Image-2(参考图)",
"endpoint": "images/generations", # 文生图端点;图生图由 provider.image_edit 走 images/edits
"unit_price": "2.0000",
"status": "active",
"metadata": {"family": "gpt", "supports_reference": True, "recommended": True},
},
)
# 切到 yunqi 唯一 active 图像模型:禁用所有非 yunqi 图像模型(含 tokenssr、火山 Seedream)。
# 否则 created_at 更早且 active 的模型会被 get_default_model(order_by created_at)顶替默认。
ModelConfig.objects.filter(capability="image").exclude(provider__name="yunqi").update(status="disabled")
def revert(apps, schema_editor):
# 反向:停用 yunqi 图像,复活 tokenssr:gpt-image-2(回到 0006 后的状态)
ModelConfig = apps.get_model("ai", "ModelConfig")
ModelConfig.objects.filter(provider__name="yunqi", capability="image").update(status="disabled")
ModelConfig.objects.filter(
provider__name="tokenssr", capability="image", name="gpt-image-2"
).update(status="active")
class Migration(migrations.Migration):
dependencies = [("ai", "0006_seed_tokenssr_models")]
operations = [migrations.RunPython(switch, revert)]
@@ -1,7 +1,6 @@
from .base import AIProvider, AIProviderResult
from .openai_compatible import OpenAICompatibleProvider
from .volcano import TtsNotConfigured, VolcanoArkProvider, VolcanoTtsProvider
from .yunqi import YunqiProvider
__all__ = [
@@ -11,5 +10,4 @@ __all__ = [
"TtsNotConfigured",
"VolcanoArkProvider",
"VolcanoTtsProvider",
"YunqiProvider",
]
@@ -24,6 +24,15 @@ class OpenAICompatibleProvider(VolcanoArkProvider):
if not self.base_url:
raise ValueError("OpenAICompatibleProvider requires base_url (中转站地址)")
def _endpoint_url(self, endpoint: str) -> str:
"""拼端点 URL。若 provider 配了 api_version(如 yunqi),附加 Azure 风格 ?api-version=...
—— yunqi 的 images/edits 不带此参数会 404。tokenssr 等不配则零影响。"""
url = f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}"
if self.api_version:
sep = "&" if "?" in url else "?"
url = f"{url}{sep}api-version={self.api_version}"
return url
def image_generation(
self,
*,
@@ -41,7 +50,7 @@ class OpenAICompatibleProvider(VolcanoArkProvider):
body["image"] = image
# 实测中转站生图延迟可达 75s+,超时给到 300s
response = requests.post(
f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}",
self._endpoint_url(endpoint),
headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"},
json=body,
timeout=300,
@@ -79,7 +88,7 @@ class OpenAICompatibleProvider(VolcanoArkProvider):
raise ValueError("image_edit 至少需要一张参考图")
data = {"model": model, "prompt": prompt, "size": size, "n": "1"}
response = requests.post(
f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}",
self._endpoint_url(endpoint),
headers={"Authorization": f"Bearer {self.api_key}"}, # multipart 不要手设 Content-Type
files=files,
data=data,
@@ -15,6 +15,9 @@ from .base import AIProviderResult
class VolcanoArkProvider:
api_key: str | None = None
base_url: str | None = None
# 部分 OpenAI 兼容中转站(如 yunqi)的生图/改图端点要求 Azure 风格的 ?api-version=... 查询参数,
# 不带会 404。火山官方直连用不到,默认 None。仅 OpenAICompatibleProvider 在拼 URL 时附加。
api_version: str | None = None
def __post_init__(self) -> None:
self.api_key = self.api_key or settings.VOLCANO.get("ark_api_key")
+4 -1
View File
@@ -66,7 +66,10 @@ def build_provider(model_config: ModelConfig):
base_url, api_key = resolve_provider_credentials(provider)
if provider.name in OFFICIAL_DIRECT_PROVIDERS:
return VolcanoArkProvider(base_url=base_url, api_key=api_key)
return OpenAICompatibleProvider(base_url=base_url, api_key=api_key)
# api_version:某些中转站(yunqi)的 images/edits 需 Azure 风格 ?api-version=...。
# DB(ModelProvider.metadata.api_version)优先 → settings(.env)回退,与凭证同款可插拔。
api_version = (provider.metadata or {}).get("api_version") or settings.PROVIDER_API_VERSIONS.get(provider.name)
return OpenAICompatibleProvider(base_url=base_url, api_key=api_key, api_version=api_version or None)
def get_image_provider(model_config: ModelConfig):