fix: Gemini 模型 fallback 链 — 修复全量降级 NVIDIA

根因: 免费层配额 GenerateRequestsPerDayPerProjectPerModel 每天每
模型仅 20 请求,日均 30+ 任务耗尽后持续 429,全部降级 NVIDIA
(最近40任务 35 nvidia / 4 gemini)。

- gemini_adapter 抽 _generate() 模型链调用: 429 立即切换下一模型
  (flash -> flash-lite,各自独立 20/天配额),503 退避 3s 重试一次
  再切换;视觉分析与问答统一走该链
- config: gemini 增 fallback_models,nvidia 恢复 nemotron-omni
  (原生视频输入;此前 Oracle 线上手动改过未回传仓库,被旧配置
  覆盖回 llama-3.2 逐帧模式) + timeout 30->120
- 健康检查校验整条模型链

实测 task 73: flash 429 -> lite 1s 内接管,12 帧直出,event_52
落库 provider=gemini。
This commit is contained in:
ericwyuan
2026-08-21 00:07:34 +08:00
parent 7efbee89fb
commit eaf4ef3bd3
2 changed files with 88 additions and 61 deletions

View File

@@ -9,6 +9,7 @@ provider_name = "gemini"
视觉分析: 多图单请求直出结构化 JSONglobal_summary/entities_json/frame_details
"""
import os
import time
import base64
import requests
from typing import Dict, List, Optional
@@ -27,6 +28,9 @@ class GeminiAdapter(BaseModelAdapter):
def __init__(self, config: dict):
super().__init__("gemini", config)
self.model_name = config.get('model_name', 'gemini-flash-latest')
# 免费层配额按模型独立20 请求/天/模型fallback 链用于跨模型借用配额
self.model_chain = [self.model_name] + [
m for m in config.get('fallback_models', []) if m and m != self.model_name]
self.api_key = self._resolve_key(config.get('api_key', ''))
self.timeout = config.get('timeout', 30)
cb_cfg = config.get('circuit_breaker', {})
@@ -52,11 +56,12 @@ class GeminiAdapter(BaseModelAdapter):
if resp.status_code == 200:
models = resp.json().get('models', [])
names = [m.get('name', '') for m in models]
if any(self.model_name in n for n in names):
logger.info(f"Gemini 健康检查通过: {self.model_name}")
missing = [m for m in self.model_chain if not any(m in n for n in names)]
if not missing:
logger.info(f"Gemini 健康检查通过: {' -> '.join(self.model_chain)}")
return True
logger.warning(f"Gemini 模型未找到: {self.model_name}; 可用: {names[:5]}")
return False
logger.warning(f"Gemini 模型未找到: {missing}; 可用: {names[:5]}")
return len(missing) < len(self.model_chain)
logger.warning(f"Gemini 健康检查 HTTP {resp.status_code}")
return False
except Exception as e:
@@ -98,43 +103,27 @@ class GeminiAdapter(BaseModelAdapter):
parts.insert(0, {"text": self._build_structured_prompt(known_members_context)})
try:
resp = requests.post(
f"{self._base_url}/models/{self.model_name}:generateContent?key={self.api_key}",
json={"contents": [{"parts": parts}],
"generationConfig": {"temperature": 0.2, "maxOutputTokens": 2048}},
timeout=self.timeout
)
if resp.status_code == 200:
cands = resp.json().get('candidates', [])
if cands:
text = ''.join(
p.get('text', '')
for p in cands[0].get('content', {}).get('parts', [])
).strip()
if not text:
logger.warning("Gemini 返回空文本")
self._cb.record_failure()
return None
try:
result = parse_vlm_json(text)
# 确保 frame_details 的 frame_timestamp 与标注一致
for f in result.get('frame_details', []):
idx = f.get('frame_index')
if isinstance(idx, int) and idx in ts_map and not f.get('frame_timestamp'):
f['frame_timestamp'] = ts_map[idx]
for f in result.get('frame_details', []):
if 'source_providers' not in f or not f.get('source_providers'):
f['source_providers'] = ['gemini']
self._cb.record_success()
logger.info(f"Gemini 视觉分析完成frame_details={len(result.get('frame_details', []))}")
return result
except VLMOutputInvalidError as e:
logger.error(f"Gemini 输出无法解析为 JSON: {e}")
self._cb.record_failure()
return None
else:
logger.warning(f"Gemini 视觉分析 HTTP {resp.status_code}: {resp.text[:150]}")
text = self._generate(parts, max_tokens=2048, temperature=0.2)
if text is None:
self._cb.record_failure()
return None
try:
result = parse_vlm_json(text)
# 确保 frame_details 的 frame_timestamp 与标注一致
for f in result.get('frame_details', []):
idx = f.get('frame_index')
if isinstance(idx, int) and idx in ts_map and not f.get('frame_timestamp'):
f['frame_timestamp'] = ts_map[idx]
for f in result.get('frame_details', []):
if 'source_providers' not in f or not f.get('source_providers'):
f['source_providers'] = ['gemini']
self._cb.record_success()
logger.info(f"Gemini 视觉分析完成frame_details={len(result.get('frame_details', []))}")
return result
except VLMOutputInvalidError as e:
logger.error(f"Gemini 输出无法解析为 JSON: {e}")
self._cb.record_failure()
return None
except requests.Timeout:
logger.warning(f"Gemini 视觉分析超时 ({self.timeout}s)")
self._cb.record_failure()
@@ -143,6 +132,58 @@ class GeminiAdapter(BaseModelAdapter):
self._cb.record_failure()
return None
def _generate(self, parts: List[dict], max_tokens: int,
temperature: float) -> Optional[str]:
"""带模型 fallback 链的 generateContent 调用
- 429每日免费配额耗尽按模型独立→ 立即换下一个模型,不重试
- 503模型过载临时性→ 同模型退避 3s 重试一次,仍失败换下一个
"""
for model in self.model_chain:
for attempt in range(2):
try:
resp = requests.post(
f"{self._base_url}/models/{model}:generateContent?key={self.api_key}",
json={"contents": [{"parts": parts}],
"generationConfig": {
"temperature": temperature, "maxOutputTokens": max_tokens}},
timeout=self.timeout
)
except requests.Timeout:
logger.warning(f"Gemini [{model}] 请求超时 ({self.timeout}s)")
break
except Exception as e:
logger.error(f"Gemini [{model}] 请求异常: {e}")
break
if resp.status_code == 200:
cands = resp.json().get('candidates', [])
text = ''.join(
p.get('text', '')
for p in (cands[0].get('content', {}) if cands else {}).get('parts', [])
).strip() if cands else ''
if text:
if model != self.model_name:
logger.info(f"Gemini 主模型不可用,由 fallback 模型 [{model}] 出结果")
return text
logger.warning(f"Gemini [{model}] 返回空文本")
continue
detail = resp.text[:150].replace('\n', ' ')
if resp.status_code == 429:
logger.warning(f"Gemini [{model}] 429 每日免费配额耗尽,切换下一模型")
break
if resp.status_code == 503:
if attempt == 0:
logger.warning(f"Gemini [{model}] 503 过载3s 后重试")
time.sleep(3)
continue
logger.warning(f"Gemini [{model}] 503 重试仍失败,切换下一模型")
break
logger.warning(f"Gemini [{model}] HTTP {resp.status_code}: {detail}")
break
return None
def _build_structured_prompt(self, known_members: str) -> str:
return f"""你是家庭监控视频分析助手。下面按时间顺序排列了多张监控截图。
请分析整个时段,只输出合法 JSON不要 markdown、不要任何解释文字结构如下
@@ -181,26 +222,10 @@ class GeminiAdapter(BaseModelAdapter):
logger.warning("Gemini API Key 未配置,跳过问答")
return None
try:
resp = requests.post(
f"{self._base_url}/models/{self.model_name}:generateContent?key={self.api_key}",
json={"contents": [{"parts": [{"text": prompt}]}],
"generationConfig": {"temperature": 0.3, "maxOutputTokens": max_tokens}},
timeout=self.timeout
)
if resp.status_code == 200:
cands = resp.json().get('candidates', [])
if cands:
text = ''.join(
p.get('text', '')
for p in cands[0].get('content', {}).get('parts', [])
).strip()
return text or None
logger.warning(f"Gemini 问答 HTTP {resp.status_code}")
except requests.Timeout:
logger.warning(f"Gemini 问答超时 ({self.timeout}s)")
return self._generate([{"text": prompt}], max_tokens=max_tokens, temperature=0.3)
except Exception as e:
logger.error(f"Gemini 问答异常: {e}")
return None
return None
def get_timeout(self) -> int:
return self.timeout