# llm_adapters.py # -*- coding: utf-8 -*- import logging from typing import Optional from langchain_openai import ChatOpenAI def ensure_openai_base_url_has_v1(url: str) -> str: import re url = url.strip() if not url: return url if not re.search(r'/v\d+$', url): if '/v1' not in url: url = url.rstrip('/') + '/v1' return url class BaseLLMAdapter: """ 统一的 LLM 接口基类,为不同后端(OpenAI、Ollama、ML Studio 等)提供一致的方法签名。 """ def invoke(self, prompt: str) -> str: raise NotImplementedError("Subclasses must implement .invoke(prompt) method.") class DeepSeekAdapter(BaseLLMAdapter): """ 适配官方/OpenAI兼容接口(使用 langchain.ChatOpenAI) """ def __init__(self, api_key: str, base_url: str, model_name: str, max_tokens: int, temperature: float = 0.7, timeout: Optional[int] = 600): self.base_url = ensure_openai_base_url_has_v1(base_url) self.api_key = api_key self.model_name = model_name self.max_tokens = max_tokens self.temperature = temperature self.timeout = timeout self._client = ChatOpenAI( model=self.model_name, api_key=self.api_key, base_url=self.base_url, max_tokens=self.max_tokens, temperature=self.temperature, timeout=self.timeout ) def invoke(self, prompt: str) -> str: response = self._client.invoke(prompt) if not response: logging.warning("No response from DeepSeekAdapter.") return "" return response.content class OpenAIAdapter(BaseLLMAdapter): """ 适配官方/OpenAI兼容接口(使用 langchain.ChatOpenAI) """ def __init__(self, api_key: str, base_url: str, model_name: str, max_tokens: int, temperature: float = 0.7, timeout: Optional[int] = 600): self.base_url = ensure_openai_base_url_has_v1(base_url) self.api_key = api_key self.model_name = model_name self.max_tokens = max_tokens self.temperature = temperature self.timeout = timeout self._client = ChatOpenAI( model=self.model_name, api_key=self.api_key, base_url=self.base_url, max_tokens=self.max_tokens, temperature=self.temperature, timeout=self.timeout ) def invoke(self, prompt: str) -> str: response = self._client.invoke(prompt) if not response: logging.warning("No response from OpenAIAdapter.") return "" return response.content class OllamaAdapter(BaseLLMAdapter): """ Ollama 同样有一个 OpenAI-like /v1/chat 接口,可直接使用 ChatOpenAI。 但是通常 Ollama 默认本地服务在 http://localhost:11434,如果符合OpenAI风格即可直接传参。 """ def __init__(self, api_key: str, base_url: str, model_name: str, max_tokens: int, temperature: float = 0.7, timeout: Optional[int] = 600): self.base_url = ensure_openai_base_url_has_v1(base_url) self.api_key = api_key self.model_name = model_name self.max_tokens = max_tokens self.temperature = temperature self.timeout = timeout self._client = ChatOpenAI( model=self.model_name, api_key=self.api_key, base_url=self.base_url, max_tokens=self.max_tokens, temperature=self.temperature, timeout=self.timeout ) def invoke(self, prompt: str) -> str: response = self._client.invoke(prompt) if not response: logging.warning("No response from OllamaAdapter.") return "" return response.content class MLStudioAdapter(BaseLLMAdapter): def __init__(self, api_key: str, base_url: str, model_name: str, max_tokens: int, temperature: float = 0.7, timeout: Optional[int] = 600): self.base_url = ensure_openai_base_url_has_v1(base_url) self.api_key = api_key self.model_name = model_name self.max_tokens = max_tokens self.temperature = temperature self.timeout = timeout self._client = ChatOpenAI( model=self.model_name, api_key=self.api_key, base_url=self.base_url, max_tokens=self.max_tokens, temperature=self.temperature, timeout=self.timeout ) def invoke(self, prompt: str) -> str: response = self._client.invoke(prompt) if not response: logging.warning("No response from MLStudioAdapter.") return "" return response.content def create_llm_adapter( interface_format: str, base_url: str, model_name: str, api_key: str, temperature: float, max_tokens: int, timeout: int ) -> BaseLLMAdapter: """ 工厂函数:根据 interface_format 返回不同的适配器实例。 """ if interface_format.lower() == "deepseek": return DeepSeekAdapter(api_key, base_url, model_name, max_tokens, temperature, timeout) elif interface_format.lower() == "openai": return OpenAIAdapter(api_key, base_url, model_name, max_tokens, temperature, timeout) elif interface_format.lower() == "ollama": return OllamaAdapter(api_key, base_url, model_name, max_tokens, temperature, timeout) elif interface_format.lower() == "ml studio": return MLStudioAdapter(api_key, base_url, model_name, max_tokens, temperature, timeout) else: raise ValueError(f"Unknown interface_format: {interface_format}")