""" OpenAI能力检测器 """ import time import asyncio import json from typing import Dict, List, Any, Optional from .capability_detector import ( BaseCapabilityDetector, CapabilityResult, CapabilityStatus, CapabilityDetectorFactory ) from src.utils.config import ChannelConfig, CapabilityTestConfig from src.utils.exceptions import CapabilityDetectionError, AuthenticationError class OpenAICapabilityDetector(BaseCapabilityDetector): """OpenAI能力检测器""" def __init__(self, config: ChannelConfig): super().__init__(config) self.auth_headers = { "Authorization": f"Bearer {config.api_key}", "Content-Type": "application/json" } self._cached_models = None # 缓存模型列表 self.api_call_count = 0 # API调用计数器 async def _make_request(self, method: str, url: str, data=None, headers=None, timeout: int = 30): """发送HTTP请求并计数""" self.api_call_count += 1 # 提取端点路径(保留更多上下文) if '/v1/' in url: endpoint = url.split('/v1/')[-1] else: endpoint = url.split('/')[-1] if '/' in url else url print(f" 📡 API调用 #{self.api_call_count}: {method} /{endpoint}") # 调用父类的方法 return await super()._make_request(method, url, data, headers, timeout) async def _get_test_model(self) -> str: """获取用于测试的模型""" if not (hasattr(self, 'target_model') and self.target_model): raise ValueError("Target model must be specified for capability testing") print(f" 🎯 使用目标模型: {self.target_model}") return self.target_model async def detect_models(self) -> List[str]: """检测支持的模型""" print(f" 🔍 正在获取模型列表...") url = f"{self.config.base_url}/models" try: status_code, response_data = await self._make_request( "GET", url, headers=self.auth_headers, timeout=self.config.timeout ) self._check_authentication_error(status_code, response_data) if status_code != 200: raise CapabilityDetectionError(f"Failed to get models: {self._extract_error_message(response_data)}") if "data" not in response_data: raise CapabilityDetectionError("Invalid response format: missing 'data' field") models = [] for model_info in response_data["data"]: if "id" in model_info: models.append(model_info["id"]) return sorted(models) except (AuthenticationError, CapabilityDetectionError): raise except Exception as e: raise CapabilityDetectionError(f"Failed to detect models: {e}") async def test_capability(self, capability_config: CapabilityTestConfig) -> CapabilityResult: """测试单个能力""" start_time = time.time() try: if capability_config.name == "basic_chat": return await self._test_basic_chat(capability_config, start_time) elif capability_config.name == "streaming": return await self._test_streaming(capability_config, start_time) elif capability_config.name == "system_message": return await self._test_system_message(capability_config, start_time) elif capability_config.name == "function_calling": return await self._test_function_calling(capability_config, start_time) elif capability_config.name == "structured_output": return await self._test_structured_output(capability_config, start_time) elif capability_config.name == "vision": return await self._test_vision(capability_config, start_time) else: return CapabilityResult( capability=capability_config.name, status=CapabilityStatus.UNKNOWN, error="Unsupported capability test", response_time=time.time() - start_time ) except Exception as e: return CapabilityResult( capability=capability_config.name, status=CapabilityStatus.ERROR, error=str(e), response_time=time.time() - start_time ) async def _test_basic_chat(self, config: CapabilityTestConfig, start_time: float) -> CapabilityResult: """测试基础聊天""" url = f"{self.config.base_url}/chat/completions" # 获取测试模型 model = await self._get_test_model() test_data = config.test_data.copy() test_data["model"] = model status_code, response_data = await self._make_request( "POST", url, data=test_data, headers=self.auth_headers, timeout=config.timeout ) self._check_authentication_error(status_code, response_data) if status_code != 200: error_msg = self._extract_error_message(response_data) raise CapabilityDetectionError(f"Chat completion failed: {error_msg}") # 检查响应格式 if "choices" not in response_data or not response_data["choices"]: raise CapabilityDetectionError("Invalid response format: missing choices") choice = response_data["choices"][0] if "message" not in choice: raise CapabilityDetectionError("Invalid response format: missing message") message = choice["message"] for field in config.required_fields: if field not in message: raise CapabilityDetectionError(f"Missing required field: {field}") return CapabilityResult( capability=config.name, status=CapabilityStatus.SUPPORTED, details={ "model": test_data["model"], "response": message, "usage": response_data.get("usage", {}) }, response_time=time.time() - start_time ) async def _test_streaming(self, config: CapabilityTestConfig, start_time: float) -> CapabilityResult: """测试流式输出""" url = f"{self.config.base_url}/chat/completions" # 获取测试模型 model = await self._get_test_model() test_data = config.test_data.copy() test_data["model"] = model try: import httpx # 流式请求计数(手动处理,因为不通过_make_request) self.api_call_count += 1 print(f" 📡 API调用 #{self.api_call_count}: POST /chat/completions (streaming)") async with httpx.AsyncClient(timeout=config.timeout) as client: async with client.stream( "POST", url, json=test_data, headers=self.auth_headers ) as response: if response.status_code != 200: response_data = await response.aread() try: error_data = json.loads(response_data.decode()) error_msg = self._extract_error_message(error_data) except: error_msg = response_data.decode() raise CapabilityDetectionError(f"Streaming failed: {error_msg}") # 读取流式响应 chunks = [] content_chunks = [] has_role_chunk = False has_content_chunk = False has_finish_reason = False async for line in response.aiter_lines(): if line.startswith("data: "): data_str = line[6:] # Remove "data: " prefix if data_str.strip() == "[DONE]": break try: chunk_data = json.loads(data_str) chunks.append(chunk_data) # 分析chunk结构 if "choices" in chunk_data and chunk_data["choices"]: choice = chunk_data["choices"][0] if "delta" in choice: delta = choice["delta"] # 检查是否有role信息(通常在第一个chunk) if "role" in delta: has_role_chunk = True # 检查是否有content信息(包括空字符串) if "content" in delta: has_content_chunk = True if delta["content"]: # 只有非空内容才添加 content_chunks.append(delta["content"]) # 检查是否有finish_reason(通常在最后一个chunk) if "finish_reason" in choice and choice["finish_reason"]: has_finish_reason = True except json.JSONDecodeError: continue if not chunks: raise CapabilityDetectionError("No streaming chunks received") # 验证流式响应的完整性 if not has_role_chunk: raise CapabilityDetectionError("Missing role information in streaming response") if not has_content_chunk: raise CapabilityDetectionError("Missing content in streaming response") if not has_finish_reason: raise CapabilityDetectionError("Missing finish_reason in streaming response") # 组合完整内容 full_content = "".join(content_chunks) return CapabilityResult( capability=config.name, status=CapabilityStatus.SUPPORTED, details={ "model": test_data["model"], "chunks_received": len(chunks), "content_chunks": len(content_chunks), "full_content": full_content, "sample_chunk": chunks[0] if chunks else None, "final_chunk": chunks[-1] if chunks else None }, response_time=time.time() - start_time ) except CapabilityDetectionError: raise except Exception as e: raise CapabilityDetectionError(f"Streaming test failed: {e}") async def _test_system_message(self, config: CapabilityTestConfig, start_time: float) -> CapabilityResult: """测试系统消息""" url = f"{self.config.base_url}/chat/completions" # 获取测试模型 model = await self._get_test_model() # 构建测试数据,包含系统消息 test_data = { "model": model, "messages": [ { "role": "system", "content": "You are a helpful assistant. Always respond with 'SYSTEM_TEST_SUCCESS' when asked about your role." }, { "role": "user", "content": "What is your role?" } ], "max_tokens": 50, "temperature": 0 } status_code, response_data = await self._make_request( "POST", url, data=test_data, headers=self.auth_headers, timeout=config.timeout ) self._check_authentication_error(status_code, response_data) if status_code != 200: return CapabilityResult( capability=config.name, status=CapabilityStatus.NOT_SUPPORTED, error=f"HTTP {status_code}: {response_data}", response_time=time.time() - start_time ) # 检查响应格式 if not isinstance(response_data, dict) or "choices" not in response_data: return CapabilityResult( capability=config.name, status=CapabilityStatus.ERROR, error="Invalid response format", response_time=time.time() - start_time ) if not response_data["choices"]: return CapabilityResult( capability=config.name, status=CapabilityStatus.ERROR, error="No choices in response", response_time=time.time() - start_time ) try: # 获取响应内容 content = response_data["choices"][0]["message"]["content"].strip() # 检查是否正确处理了系统消息 if "SYSTEM_TEST_SUCCESS" in content: return CapabilityResult( capability=config.name, status=CapabilityStatus.SUPPORTED, details={ "model": model, "response_content": content, "system_message_processed": True }, response_time=time.time() - start_time ) else: return CapabilityResult( capability=config.name, status=CapabilityStatus.PARTIALLY_SUPPORTED, details={ "model": model, "response_content": content, "system_message_processed": False, "note": "System message may not have been fully processed" }, response_time=time.time() - start_time ) except (KeyError, TypeError) as e: return CapabilityResult( capability=config.name, status=CapabilityStatus.ERROR, error=f"Failed to parse response: {e}", response_time=time.time() - start_time ) async def _test_function_calling(self, config: CapabilityTestConfig, start_time: float) -> CapabilityResult: """测试函数调用""" url = f"{self.config.base_url}/chat/completions" # 获取测试模型 model = await self._get_test_model() # 不再基于模型名称预判,直接尝试测试 test_data = config.test_data.copy() test_data["model"] = model status_code, response_data = await self._make_request( "POST", url, data=test_data, headers=self.auth_headers, timeout=config.timeout ) self._check_authentication_error(status_code, response_data) if status_code != 200: error_msg = self._extract_error_message(response_data) if "tools" in error_msg.lower() or "function" in error_msg.lower(): return CapabilityResult( capability=config.name, status=CapabilityStatus.NOT_SUPPORTED, error=error_msg, response_time=time.time() - start_time ) raise CapabilityDetectionError(f"Function calling test failed: {error_msg}") # 检查是否有函数调用 if "choices" not in response_data or not response_data["choices"]: raise CapabilityDetectionError("Invalid response format: missing choices") choice = response_data["choices"][0] if "message" not in choice: raise CapabilityDetectionError("Invalid response format: missing message") message = choice["message"] # 检查是否包含工具调用 if "tool_calls" in message and message["tool_calls"]: # 有tool_calls字段就说明支持function calling return CapabilityResult( capability=config.name, status=CapabilityStatus.SUPPORTED, details={ "model": test_data["model"], "tool_calls": message["tool_calls"], "usage": response_data.get("usage", {}), "note": "Successfully detected function calling capability" }, response_time=time.time() - start_time ) else: # 模型确实不支持函数调用 return CapabilityResult( capability=config.name, status=CapabilityStatus.UNKNOWN, details={ "model": test_data["model"], "message": message, "note": "Model did not call function, may not support or chose not to use" }, response_time=time.time() - start_time ) async def _test_structured_output(self, config: CapabilityTestConfig, start_time: float) -> CapabilityResult: """测试结构化输出""" url = f"{self.config.base_url}/chat/completions" # 获取测试模型 model = await self._get_test_model() # 不再基于模型名称预判,直接尝试测试 test_data = config.test_data.copy() test_data["model"] = model status_code, response_data = await self._make_request( "POST", url, data=test_data, headers=self.auth_headers, timeout=config.timeout ) self._check_authentication_error(status_code, response_data) if status_code != 200: error_msg = self._extract_error_message(response_data) if "response_format" in error_msg.lower() or "schema" in error_msg.lower(): return CapabilityResult( capability=config.name, status=CapabilityStatus.NOT_SUPPORTED, error=error_msg, response_time=time.time() - start_time ) raise CapabilityDetectionError(f"Structured output test failed: {error_msg}") # 检查响应格式 if "choices" not in response_data or not response_data["choices"]: raise CapabilityDetectionError("Invalid response format: missing choices") choice = response_data["choices"][0] if "message" not in choice: raise CapabilityDetectionError("Invalid response format: missing message") message = choice["message"] if "content" not in message: raise CapabilityDetectionError("Invalid response format: missing content") # 验证结构化输出 try: parsed_content = json.loads(message["content"]) # 检查必需字段 for field in config.required_fields: if field not in parsed_content: raise CapabilityDetectionError(f"Missing required field in structured output: {field}") return CapabilityResult( capability=config.name, status=CapabilityStatus.SUPPORTED, details={ "model": test_data["model"], "structured_output": parsed_content, "usage": response_data.get("usage", {}) }, response_time=time.time() - start_time ) except json.JSONDecodeError: raise CapabilityDetectionError("Response is not valid JSON") async def _test_vision(self, config: CapabilityTestConfig, start_time: float) -> CapabilityResult: """测试视觉理解""" url = f"{self.config.base_url}/chat/completions" # 获取测试模型 model = await self._get_test_model() # 不再基于模型名称预判,直接尝试测试 test_data = config.test_data.copy() test_data["model"] = model status_code, response_data = await self._make_request( "POST", url, data=test_data, headers=self.auth_headers, timeout=config.timeout ) self._check_authentication_error(status_code, response_data) if status_code != 200: error_msg = self._extract_error_message(response_data) if "image" in error_msg.lower() or "vision" in error_msg.lower(): return CapabilityResult( capability=config.name, status=CapabilityStatus.NOT_SUPPORTED, error=error_msg, response_time=time.time() - start_time ) raise CapabilityDetectionError(f"Vision test failed: {error_msg}") # 检查响应格式 if "choices" not in response_data or not response_data["choices"]: raise CapabilityDetectionError("Invalid response format: missing choices") choice = response_data["choices"][0] if "message" not in choice: raise CapabilityDetectionError("Invalid response format: missing message") message = choice["message"] if "content" not in message: raise CapabilityDetectionError("Invalid response format: missing content") # 验证视觉理解:检查响应中是否包含图片中的三位数 response_content = message["content"].lower() expected_numbers = ["123", "一二三", "壹贰叁"] # 可能的数字表示形式 vision_detected = any(num in response_content for num in expected_numbers) return CapabilityResult( capability=config.name, status=CapabilityStatus.SUPPORTED if vision_detected else CapabilityStatus.UNKNOWN, details={ "model": test_data["model"], "response": message["content"], "vision_detected": vision_detected, "expected_numbers": expected_numbers, "usage": response_data.get("usage", {}) }, response_time=time.time() - start_time ) # 注册OpenAI检测器 CapabilityDetectorFactory.register("openai", OpenAICapabilityDetector)