feat: 接入模型动态 Fallback 与调用审计
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user