test_qwen_video.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. """通义千问视频 client:provider 开关 + video_url 取 oss + 解析。"""
  2. import json
  3. import httpx
  4. from content_agent.integrations.qwen_video import QwenVideoClient
  5. from content_agent.run_service import _gemini_video_client_from_env
  6. from content_agent.integrations.gemini_video import GeminiVideoClient
  7. from content_agent.integrations import timeout_config
  8. class _FakeResponse:
  9. def __init__(self, payload, *, status_code=200, headers=None):
  10. self._payload = payload
  11. self.status_code = status_code
  12. self.headers = headers or {"content-type": "application/json"}
  13. self.content = json.dumps(payload).encode()
  14. self.request = httpx.Request("POST", "https://dashscope.test/chat/completions")
  15. def raise_for_status(self):
  16. if self.status_code >= 400:
  17. raise httpx.HTTPStatusError("bad", request=self.request, response=self)
  18. return None
  19. def json(self):
  20. return self._payload
  21. def test_provider_switch_picks_qwen_vs_openrouter():
  22. qwen = _gemini_video_client_from_env({"CONTENT_AGENT_VIDEO_LLM_PROVIDER": "dashscope", "CONTENT_AGENT_VIDEO_LLM_API_KEY": "k"})
  23. assert isinstance(qwen, QwenVideoClient)
  24. openrouter = _gemini_video_client_from_env({"OPENROUTER_API_KEY": "k"})
  25. assert isinstance(openrouter, GeminiVideoClient)
  26. def test_qwen_analyze_sends_video_url_oss_and_parses():
  27. captured = {}
  28. def fake_post(url, headers=None, json=None, timeout=None):
  29. captured["url"] = url
  30. captured["json"] = json
  31. captured["timeout"] = timeout
  32. return _FakeResponse(
  33. {"choices": [{"message": {"content": '{"query_relevance_score": 88, "query_relevance_reason": "贴合"}'}}]}
  34. )
  35. client = QwenVideoClient(api_key="k", model="qwen3-vl-plus", http_post=fake_post)
  36. out = client.analyze(
  37. {"platform": "douyin", "search_query": "拉伸"},
  38. {"oss_url": "https://res.example.com/v.mp4", "play_url": "https://douyinvod.com/x"},
  39. {},
  40. )
  41. assert out["final_status"] == "ok"
  42. assert out["query_relevance_score"] == 88
  43. # 发的是 dashscope chat/completions,且用 video_url 取 oss(不是 play_url、不是 base64)
  44. assert captured["url"].endswith("/chat/completions")
  45. content = captured["json"]["messages"][1]["content"]
  46. video_part = next(p for p in content if p["type"] == "video_url")
  47. assert video_part["video_url"]["url"] == "https://res.example.com/v.mp4"
  48. assert captured["timeout"].read == timeout_config.read_timeout("video_llm") == 120.0
  49. def test_content_inspection_split_detection():
  50. from content_agent.flow_ledger_service import _is_content_inspection_blocked
  51. # 新数据:独立 failure_type
  52. assert _is_content_inspection_blocked({"failure_type": "content_inspection_blocked"}) is True
  53. # 旧数据(改之前跑的 run):从响应体文本兜底识别
  54. assert _is_content_inspection_blocked(
  55. {"failure_type": "qwen_http_error", "openrouter": {"response_summary": {"text_excerpt": "<400> InternalError.Algo.DataInspectionFailed: Input video data may contain inappropriate content."}}}
  56. ) is True
  57. # 普通技术问题不误判
  58. assert _is_content_inspection_blocked({"failure_type": "no_valid_play_url"}) is False
  59. assert _is_content_inspection_blocked(None) is False
  60. def test_qwen_missing_oss_returns_failed():
  61. client = QwenVideoClient(api_key="k", http_post=lambda *a, **k: None)
  62. out = client.analyze({"platform": "douyin"}, {"play_url": "https://douyinvod.com/x"}, {})
  63. assert out["final_status"] == "failed" # 只有 play_url(模型取不到)→ 视作无源
  64. def test_qwen_429_retries_three_times_and_keeps_retry_after():
  65. calls = []
  66. def fake_post(*args, **kwargs):
  67. calls.append(kwargs)
  68. return _FakeResponse(
  69. {"error": {"message": "too many"}},
  70. status_code=429,
  71. headers={"content-type": "application/json", "retry-after": "7"},
  72. )
  73. client = QwenVideoClient(api_key="k", http_post=fake_post)
  74. out = client.analyze({"platform": "douyin"}, {"oss_url": "https://res.example.com/v.mp4"}, {})
  75. assert len(calls) == 3
  76. assert out["failure_type"] == "qwen_http_error"
  77. assert out["http_status_code"] == 429
  78. assert out["qwen_retry_after_seconds"] == 7.0
  79. assert out["retry_count"] == 3
  80. assert out["response_body_summary"]["retry_after"] == "7"
  81. def test_qwen_content_inspection_does_not_retry():
  82. calls = []
  83. def fake_post(*args, **kwargs):
  84. calls.append(kwargs)
  85. return _FakeResponse(
  86. {"error": {"message": "InternalError.Algo.DataInspectionFailed: inappropriate content"}},
  87. status_code=400,
  88. )
  89. client = QwenVideoClient(api_key="k", http_post=fake_post)
  90. out = client.analyze({"platform": "douyin"}, {"oss_url": "https://res.example.com/v.mp4"}, {})
  91. assert len(calls) == 1
  92. assert out["failure_type"] == "content_inspection_blocked"
  93. assert out.get("qwen_retryable") is False