feat: 接入模型动态 Fallback 与调用审计

This commit is contained in:
hh
2026-07-21 10:35:50 +08:00
parent 9429133d04
commit d0c3690d47
45 changed files with 9253 additions and 173 deletions
@@ -86,6 +86,7 @@ class OpenAICompatibleProvider(VolcanoArkProvider):
endpoint: str = "images/generations",
image: str | list[str] | None = None,
size: str = "1024x1536",
timeout: float = 300,
) -> dict[str, Any]:
"""文生图(可选单图参考 base64)。多图参考请用 image_edit。返回体含 url 或 b64_json。"""
if not self.api_key:
@@ -98,7 +99,7 @@ class OpenAICompatibleProvider(VolcanoArkProvider):
self._endpoint_url(endpoint),
headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"},
json=body,
timeout=300,
timeout=timeout,
)
_raise_with_body(response)
return response.json()
@@ -111,6 +112,7 @@ class OpenAICompatibleProvider(VolcanoArkProvider):
images: list[str],
endpoint: str = "images/edits",
size: str = "1024x1536",
timeout: float = 300,
) -> dict[str, Any]:
"""参考图编辑/合成(gpt-image-2 核心能力):multipart `image[]` 上传一张或多张参考图。
@@ -139,7 +141,47 @@ class OpenAICompatibleProvider(VolcanoArkProvider):
headers={"Authorization": f"Bearer {self.api_key}"}, # multipart 不要手设 Content-Type
files=files,
data=data,
timeout=300,
timeout=timeout,
)
_raise_with_body(response)
return response.json()
def synthesize(
self,
*,
model: str,
text: str,
voice_type: str,
speed_ratio: float = 1.0,
endpoint: str = "audio/speech",
output_format: str = "mp3",
timeout: float = 60,
uid: str = "airshelf",
) -> tuple[bytes, int]:
"""OpenAI 兼容 ``audio/speech``:返回音频字节和可选时长毫秒。
``uid`` 仅为与火山直连调用签名一致,不发送给标准 OpenAI 请求。新增供应商只需在
ModelConfig 配 endpoint / voice_map,无需新增 Provider 分支。
"""
del uid
if not self.api_key:
raise ValueError("中转站 api_key 未配置")
response = requests.post(
self._endpoint_url(endpoint or "audio/speech"),
headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"},
json={
"model": model,
"input": text,
"voice": voice_type,
"speed": float(speed_ratio or 1.0),
"response_format": output_format,
},
timeout=timeout,
)
_raise_with_body(response)
duration_ms = 0
try:
duration_ms = int(float(response.headers.get("x-audio-duration-ms") or 0))
except (TypeError, ValueError):
duration_ms = 0
return response.content, duration_ms
+33 -9
View File
@@ -59,7 +59,14 @@ class VolcanoArkProvider:
payload=data,
)
def chat_completion(self, *, model: str, messages: list[dict[str, str]], endpoint: str = "chat/completions") -> dict[str, Any]:
def chat_completion(
self,
*,
model: str,
messages: list[dict[str, str]],
endpoint: str = "chat/completions",
timeout: float = 120,
) -> dict[str, Any]:
if not self.api_key:
raise ValueError("VOLCANO_ARK_API_KEY is not configured")
@@ -67,7 +74,7 @@ class VolcanoArkProvider:
f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}",
headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"},
json={"model": model, "messages": messages},
timeout=120,
timeout=timeout,
)
response.raise_for_status()
return response.json()
@@ -80,6 +87,7 @@ class VolcanoArkProvider:
endpoint: str = "chat/completions",
temperature: float = 0.8,
extra_body: dict[str, Any] | None = None,
timeout: float = 300,
) -> Iterator[dict[str, Any]]:
"""流式对话:逐块 yield {type:'delta'|'tool_call'|'done', ...}。
OpenAI 兼容 SSE(火山 ARK / 各中转站同构),供脚本 agent 的 SSE 端实时转发。"""
@@ -97,7 +105,7 @@ class VolcanoArkProvider:
},
json=body,
stream=True,
timeout=300,
timeout=timeout,
) as response:
response.raise_for_status()
# SSE 响应常不带 charset,requests 会按 latin-1 解码 → 中文乱码。强制 UTF-8。
@@ -157,6 +165,7 @@ class VolcanoArkProvider:
endpoint: str = "images/generations",
image: str | list[str] | None = None,
size: str = "2K",
timeout: float = 180,
) -> dict[str, Any]:
if not self.api_key:
raise ValueError("VOLCANO_ARK_API_KEY is not configured")
@@ -174,7 +183,7 @@ class VolcanoArkProvider:
f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}",
headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"},
json=body,
timeout=180,
timeout=timeout,
)
# 非 2xx 抽出火山真实报错(error.code/message)而非裸 HTTP 400 —— 否则 Seedream 的
# 内容审核 / 尺寸过小(历史 4:5 套图 raise_for_status 吞 body 的踩坑)等真因看不见。
@@ -195,6 +204,7 @@ class VolcanoArkProvider:
content_items: list[dict[str, Any]] | None = None,
seed: int | None = None,
search_mode: str = "off",
timeout: float = 120,
) -> dict[str, Any]:
if not self.api_key:
raise ValueError("VOLCANO_ARK_API_KEY is not configured")
@@ -223,18 +233,24 @@ class VolcanoArkProvider:
f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}",
headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"},
json=body,
timeout=120,
timeout=timeout,
)
_raise_volcano_error(response)
return response.json()
def poll_video_task(self, *, endpoint: str, provider_task_id: str) -> dict[str, Any]:
def poll_video_task(
self,
*,
endpoint: str,
provider_task_id: str,
timeout: float = 60,
) -> dict[str, Any]:
if not self.api_key:
raise ValueError("VOLCANO_ARK_API_KEY is not configured")
response = requests.get(
f"{self.base_url.rstrip('/')}/{endpoint.rstrip('/')}/{provider_task_id}",
headers={"Authorization": f"Bearer {self.api_key}"},
timeout=60,
timeout=timeout,
)
_raise_volcano_error(response)
return response.json()
@@ -305,7 +321,15 @@ class VolcanoTtsProvider:
def configured(self) -> bool:
return bool(self.appid and self.access_token)
def synthesize(self, *, text: str, voice_type: str, speed_ratio: float = 1.0, uid: str = "airshelf") -> tuple[bytes, int]:
def synthesize(
self,
*,
text: str,
voice_type: str,
speed_ratio: float = 1.0,
uid: str = "airshelf",
timeout: float = 60,
) -> tuple[bytes, int]:
"""合成一段语音。返回 (mp3 字节, 时长毫秒;接口没回时长则为 0)。"""
if not self.configured:
raise TtsNotConfigured(
@@ -322,7 +346,7 @@ class VolcanoTtsProvider:
self.base_url,
headers={"Authorization": f"Bearer;{self.access_token}"},
json=body,
timeout=60,
timeout=timeout,
)
response.raise_for_status()
data = response.json()
+2 -1
View File
@@ -26,6 +26,7 @@ class YunqiProvider(VolcanoArkProvider):
endpoint: str = "images/generations",
image: str | list[str] | None = None,
size: str = "1024x1536",
timeout: float = 300,
) -> dict[str, Any]:
if not self.api_key:
raise ValueError("YUNQI_API_KEY is not configured")
@@ -37,7 +38,7 @@ class YunqiProvider(VolcanoArkProvider):
f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}",
headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"},
json=body,
timeout=300,
timeout=timeout,
)
response.raise_for_status()
return response.json()