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:
@@ -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")
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user