Добавление интеграции OpenRouter AI

This commit is contained in:
2026-01-15 12:19:29 +03:00
parent 401993526b
commit a23d714128
8 changed files with 571 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
from dataclasses import dataclass
from typing import Optional
from django.conf import settings
import os
import logging
logger = logging.getLogger(__name__)
@dataclass
class AIConfig:
"""
Конфигурация для интеграции с ИИ-сервисами
"""
api_key: str
api_url: str
model_name: str
temperature: float
is_coding_endpoint: bool = False
max_tokens: int = 1000
timeout: int = 30
@classmethod
def from_integration_model(cls, integration_model):
"""
Создать конфигурацию из модели интеграции
"""
logger.info(f"=== Создание конфигурации из модели интеграции ===")
logger.info(f"Тип модели: {type(integration_model)}")
logger.info(f"Атрибуты модели: {dir(integration_model)}")
try:
api_key = integration_model.api_key
logger.info(f"API key (первые 10 символов): {api_key[:10] if api_key else 'None'}...")
except AttributeError as e:
logger.error(f"Ошибка при получении api_key: {str(e)}")
raise
# Всегда используем общий эндпоинт
api_url = "https://api.z.ai/api/paas/v4"
logger.info(f"Используем общий endpoint: {api_url}")
try:
model_name = integration_model.model_name
logger.info(f"Model name: {model_name}")
except AttributeError as e:
logger.error(f"Ошибка при получении model_name: {str(e)}")
raise
try:
temperature = float(integration_model.temperature)
logger.info(f"Temperature: {temperature}")
except AttributeError as e:
logger.error(f"Ошибка при получении temperature: {str(e)}")
raise
return cls(
api_key=api_key,
api_url=api_url,
model_name=model_name,
temperature=temperature,
is_coding_endpoint=False, # Всегда False, так как используем общий endpoint
)
@classmethod
def from_env(cls):
"""
Создать конфигурацию из переменных окружения
"""
return cls(
api_key=os.getenv('ZAI_API_KEY', ''),
api_url=os.getenv('ZAI_API_URL', 'https://api.z.ai/api/paas/v4'),
model_name=os.getenv('ZAI_MODEL_NAME', 'glm-4.7'),
temperature=float(os.getenv('ZAI_TEMPERATURE', '0.7')),
is_coding_endpoint=os.getenv('ZAI_CODING_ENDPOINT', 'false').lower() == 'true',
)
def get_glm_config(integration_model=None):
"""
Получить конфигурацию GLM из модели интеграции или из .env
"""
if integration_model:
return AIConfig.from_integration_model(integration_model)
else:
return AIConfig.from_env()
@dataclass
class OpenRouterConfig:
"""
Конфигурация для интеграции с OpenRouter
"""
api_key: str
api_url: str
model_name: str
temperature: float
max_tokens: int = 1000
timeout: int = 30
@classmethod
def from_integration_model(cls, integration_model):
"""
Создать конфигурацию из модели интеграции
"""
logger.info(f"=== Создание конфигурации OpenRouter из модели интеграции ===")
logger.info(f"Тип модели: {type(integration_model)}")
try:
api_key = integration_model.api_key
logger.info(f"API key (первые 10 символов): {api_key[:10] if api_key else 'None'}...")
except AttributeError as e:
logger.error(f"Ошибка при получении api_key: {str(e)}")
raise
api_url = getattr(integration_model, 'api_url', 'https://openrouter.ai/api/v1')
logger.info(f"API URL: {api_url}")
try:
model_name = integration_model.model_name
logger.info(f"Model name: {model_name}")
except AttributeError as e:
logger.error(f"Ошибка при получении model_name: {str(e)}")
raise
try:
temperature = float(integration_model.temperature)
logger.info(f"Temperature: {temperature}")
except AttributeError as e:
logger.error(f"Ошибка при получении temperature: {str(e)}")
raise
max_tokens = getattr(integration_model, 'max_tokens', 1000)
logger.info(f"Max tokens: {max_tokens}")
return cls(
api_key=api_key,
api_url=api_url,
model_name=model_name,
temperature=temperature,
max_tokens=max_tokens,
)
@classmethod
def from_env(cls):
"""
Создать конфигурацию из переменных окружения
"""
return cls(
api_key=os.getenv('OPENROUTER_API_KEY', ''),
api_url=os.getenv('OPENROUTER_API_URL', 'https://openrouter.ai/api/v1'),
model_name=os.getenv('OPENROUTER_MODEL_NAME', 'xiaomi/mimo-v2-flash:free'),
temperature=float(os.getenv('OPENROUTER_TEMPERATURE', '0.7')),
max_tokens=int(os.getenv('OPENROUTER_MAX_TOKENS', '1000')),
)
def get_openrouter_config(integration_model=None):
"""
Получить конфигурацию OpenRouter из модели интеграции или из .env
"""
if integration_model:
return OpenRouterConfig.from_integration_model(integration_model)
else:
return OpenRouterConfig.from_env()

View File

@@ -0,0 +1,200 @@
from typing import Dict, Any, Tuple, Optional
from ..base import BaseIntegrationService
from .config import get_openrouter_config
import logging
import sys
import locale
# Патч для исправления проблемы с кодировкой в httpx на Windows
# Устанавливаем кодировку по умолчанию для Python
if sys.platform == 'win32':
try:
import httpx._models
original_normalize_header_value = httpx._models._normalize_header_value
def patched_normalize_header_value(value, encoding):
"""Патч для использования UTF-8 вместо ASCII для заголовков"""
# Если значение уже bytes, возвращаем его как есть
if isinstance(value, bytes):
return value
# Всегда используем UTF-8 вместо ASCII
encoding = encoding or 'utf-8'
if encoding.lower() == 'ascii':
encoding = 'utf-8'
return value.encode(encoding)
httpx._models._normalize_header_value = patched_normalize_header_value
logging.getLogger(__name__).info("Applied patch for httpx header encoding on Windows")
except Exception as e:
logging.getLogger(__name__).warning(f"Failed to apply httpx patch: {e}")
from openai import OpenAI
logger = logging.getLogger(__name__)
class OpenRouterIntegrationService(BaseIntegrationService):
"""
Сервис интеграции с OpenRouter.ai
"""
def __init__(self, config):
super().__init__(config)
logger.info(f"=== Инициализация OpenRouterIntegrationService ===")
try:
self.cfg = get_openrouter_config(config)
logger.info(f"Конфигурация успешно получена")
logger.info(f"API URL: {self.cfg.api_url}")
logger.info(f"Model name: {self.cfg.model_name}")
logger.info(f"Temperature: {self.cfg.temperature}")
logger.info(f"API key (первые 10 символов): {self.cfg.api_key[:10] if self.cfg.api_key else 'None'}...")
except Exception as e:
logger.error(f"Ошибка при получении конфигурации: {str(e)}", exc_info=True)
raise
# Импортируем клиент OpenAI (OpenRouter совместим с OpenAI API)
try:
self.client = OpenAI(
api_key=self.cfg.api_key,
base_url=self.cfg.api_url,
)
logger.info(f"OpenAI клиент для OpenRouter успешно создан")
except ImportError:
logger.error("Библиотека openai не установлена")
raise ImportError(
"Необходимо установить библиотеку openai: pip install openai"
)
except Exception as e:
logger.error(f"Ошибка при создании клиента: {str(e)}", exc_info=True)
raise
def test_connection(self) -> Tuple[bool, str]:
"""
Проверить соединение с API OpenRouter
"""
logger.info(f"=== Начало тестирования соединения с OpenRouter ===")
try:
logger.info(f"Отправка запроса к API OpenRouter...")
logger.info(f"Model: {self.cfg.model_name}, Temperature: {self.cfg.temperature}")
messages = [{"role": "user", "content": "ping"}]
response = self.client.chat.completions.create(
model=self.cfg.model_name,
messages=messages,
temperature=self.cfg.temperature,
max_tokens=10
)
logger.info(f"Получен ответ от API OpenRouter")
if response.choices and len(response.choices) > 0:
logger.info(f"Успешное подключение к OpenRouter")
return True, "Connection to OpenRouter successful"
else:
logger.error(f"Некорректный ответ от API OpenRouter: нет choices")
return False, "Invalid response from OpenRouter API"
except Exception as e:
logger.error(f"Ошибка подключения к OpenRouter: {str(e)}", exc_info=True)
return False, f"Connection error: {str(e)}"
def sync(self) -> Tuple[bool, str]:
"""
Основная операция синхронизации (для OpenRouter не требуется)
"""
return True, "Синхронизация OpenRouter не требуется"
def generate_text(self,
prompt: str,
system_prompt: Optional[str] = None,
max_tokens: Optional[int] = None) -> Tuple[bool, str, Optional[Dict]]:
"""
Генерация текста с помощью OpenRouter
Args:
prompt: Входной текст для генерации
system_prompt: Системный промпт (опционально)
max_tokens: Максимальное количество токенов в ответе
Returns:
tuple: (success: bool, message: str, response_data: dict or None)
"""
if not self.is_available():
return False, "Интеграция OpenRouter не активна", None
try:
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": prompt})
response = self.client.chat.completions.create(
model=self.cfg.model_name,
messages=messages,
temperature=self.cfg.temperature,
max_tokens=max_tokens or self.cfg.max_tokens
)
generated_text = response.choices[0].message.content
usage_info = getattr(response, 'usage', {})
return True, "Текст успешно сгенерирован", {
'generated_text': generated_text,
'usage': usage_info,
'model': self.cfg.model_name
}
except Exception as e:
logger.error(f"Ошибка генерации текста с помощью OpenRouter: {str(e)}")
return False, f"Ошибка генерации: {str(e)}", None
def generate_code(self,
prompt: str,
language: Optional[str] = None,
max_tokens: Optional[int] = None) -> Tuple[bool, str, Optional[Dict]]:
"""
Генерация кода с помощью OpenRouter
Args:
prompt: Описание задачи для генерации кода
language: Язык программирования (опционально)
max_tokens: Максимальное количество токенов в ответе
Returns:
tuple: (success: bool, message: str, response_data: dict or None)
"""
if not self.is_available():
return False, "Интеграция OpenRouter не активна", None
try:
system_prompt = "You are a helpful AI coding assistant. Generate clean, efficient, and well-documented code."
if language:
system_prompt += f" The code should be in {language}."
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
]
response = self.client.chat.completions.create(
model=self.cfg.model_name,
messages=messages,
temperature=self.cfg.temperature,
max_tokens=max_tokens or self.cfg.max_tokens
)
generated_code = response.choices[0].message.content
usage_info = getattr(response, 'usage', {})
return True, "Код успешно сгенерирован", {
'generated_code': generated_code,
'usage': usage_info,
'model': self.cfg.model_name
}
except Exception as e:
logger.error(f"Ошибка генерации кода с помощью OpenRouter: {str(e)}")
return False, f"Ошибка генерации кода: {str(e)}", None