680 lines
31 KiB
Python
680 lines
31 KiB
Python
"""
|
||
LLM服务 - 重构版本
|
||
提供大语言模型的扩写和翻译功能
|
||
继承OpenAICompatibleService以复用通用逻辑
|
||
"""
|
||
|
||
import time
|
||
import asyncio
|
||
from typing import Optional, Dict, Any, List, Callable
|
||
from .openai_base import OpenAICompatibleService
|
||
from .thinking_filter import postprocess_model_output
|
||
from ..utils.common import (
|
||
format_api_error, ProgressBar, log_complete, log_error,
|
||
PREFIX, PROCESS_PREFIX, WARN_PREFIX, ERROR_PREFIX, format_elapsed_time,
|
||
TASK_EXPAND, TASK_TRANSLATE
|
||
)
|
||
from .thinking_control import build_thinking_suppression, should_append_no_thinking_instruction
|
||
|
||
|
||
class LLMService(OpenAICompatibleService):
|
||
"""
|
||
大语言模型服务
|
||
支持提示词扩写和文本翻译
|
||
"""
|
||
|
||
@staticmethod
|
||
def _get_config() -> Dict[str, Any]:
|
||
"""获取LLM配置"""
|
||
from ..config_manager import config_manager
|
||
config = config_manager.get_llm_config()
|
||
current_provider = config.get('provider')
|
||
|
||
if 'providers' in config and current_provider in config['providers']:
|
||
provider_config = config['providers'][current_provider]
|
||
return {
|
||
'provider': current_provider,
|
||
'model': provider_config.get('model', ''),
|
||
'base_url': provider_config.get('base_url', ''),
|
||
'api_key': provider_config.get('api_key', ''),
|
||
'temperature': provider_config.get('temperature', 0.7),
|
||
'top_p': provider_config.get('top_p', 0.9),
|
||
'max_tokens': provider_config.get('max_tokens', 2000),
|
||
'auto_unload': provider_config.get('auto_unload', True)
|
||
}
|
||
else:
|
||
return config
|
||
|
||
@staticmethod
|
||
def _is_chinese(text: str) -> bool:
|
||
"""判断文本是否包含中文"""
|
||
return any('\u4e00' <= char <= '\u9fff' for char in text)
|
||
|
||
@staticmethod
|
||
async def _call_ollama_native(
|
||
model: str,
|
||
messages: List[Dict[str, str]],
|
||
temperature: float,
|
||
top_p: float,
|
||
max_tokens: int,
|
||
base_url: str,
|
||
stream_callback: Optional[Callable[[str], None]] = None,
|
||
request_id: Optional[str] = None,
|
||
provider_display_name: str = "Ollama",
|
||
auto_unload: bool = True,
|
||
enable_advanced_params: bool = False,
|
||
thinking_extra: Optional[Dict[str, Any]] = None,
|
||
filter_thinking_output: bool = True,
|
||
cancel_event: Optional[Any] = None,
|
||
task_type: str = None,
|
||
source: str = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
调用Ollama原生API(支持流式输出)
|
||
用于支持智能上下文窗口和思维链控制
|
||
|
||
参数:
|
||
enable_advanced_params: 是否发送高级参数(temperature/top_p/num_predict)
|
||
thinking_extra: 思维链控制参数
|
||
"""
|
||
# ---初始化请求参数---
|
||
|
||
try:
|
||
start_time = time.perf_counter()
|
||
_thinking_extra = thinking_extra # 使用传入的参数
|
||
_thinking_tag = "(已关闭思维链)" if _thinking_extra else ""
|
||
|
||
# 计算基准 URL (确保移除 /v1 和末尾斜杠)
|
||
native_base = base_url.rstrip('/') if base_url else 'http://localhost:11434'
|
||
if native_base.endswith('/v1'):
|
||
native_base = native_base[:-3].rstrip('/')
|
||
|
||
# 智能动态上下文窗口计算 (Token估算策略: 中文0.7/char, 英文0.3/char -> 保守取 0.6/char)
|
||
# 安全地计算输入长度 (包含 System Prompt 和 User Prompt,前提是都在 messages 中)
|
||
input_char_len = 0
|
||
for msg in messages:
|
||
input_char_len += len(msg.get('content', '') or '')
|
||
|
||
estimated_input_tokens = int(input_char_len * 0.6)
|
||
|
||
# --- 智能预留策略 ---
|
||
# 关键点:思考过程 (Thinking Process) 也占用 Output Token 额度
|
||
# 1. 如果成功禁用了思维链 (_thinking_extra 非空) -> 安全
|
||
# 2. 如果模型名明确包含 instruct/chat (通常无思考过程) -> 安全
|
||
# 3. 其他未知模型 -> 假定可能存在思考过程,预留更多空间
|
||
|
||
is_safe_standard_model = False
|
||
if model:
|
||
m = model.lower()
|
||
if "instruct" in m or "chat" in m:
|
||
is_safe_standard_model = True
|
||
|
||
if _thinking_extra or is_safe_standard_model:
|
||
# 已关闭思维链 OR 标准指令模型 -> 极致节省模式
|
||
min_output = 512
|
||
min_ctx = 1024
|
||
else:
|
||
# 未知/潜在思考模型 -> 安全能够模式 (为思考过程预留空间)
|
||
min_output = 1024
|
||
min_ctx = 2048
|
||
|
||
# 任务类型预留
|
||
output_reserve = max(min_output, int(estimated_input_tokens * 1.5))
|
||
|
||
# 384为系统开销Buffer (System Prompt通常已在estimated_input_tokens中,这里是额外的安全余量)
|
||
required_ctx = estimated_input_tokens + output_reserve + 384
|
||
|
||
# 对齐到 1024 倍数
|
||
# 限制在 [min_ctx, 32768] 范围内
|
||
num_ctx = max(min_ctx, min(32768, required_ctx))
|
||
num_ctx = ((num_ctx + 1023) // 1024) * 1024
|
||
|
||
# 合并多条 System Message(Ollama 对多条 system 消息处理不佳)
|
||
merged_messages = LLMService._merge_system_prompts(messages)
|
||
|
||
# 构建请求(安全地构建messages列表)
|
||
ollama_messages = []
|
||
for msg in merged_messages:
|
||
ollama_messages.append({
|
||
"role": msg.get('role', 'user'),
|
||
"content": msg.get('content', '')
|
||
})
|
||
|
||
# 构建基础请求体
|
||
payload = {
|
||
"model": model,
|
||
"messages": ollama_messages,
|
||
"stream": True
|
||
}
|
||
|
||
# ---构建 options---
|
||
# 基础参数:num_ctx(动态上下文窗口大小)
|
||
options = {
|
||
"num_ctx": num_ctx
|
||
}
|
||
|
||
# 高级参数:仅在用户启用时发送
|
||
# 参数说明(基于 Ollama 官方文档):
|
||
# - temperature: 控制随机性,默认0.8,值越低输出越稳定
|
||
# - top_p: 核采样,默认0.9,限制候选词概率范围
|
||
# - num_predict: 最大生成Token数,默认-1(无限)
|
||
if enable_advanced_params:
|
||
options["temperature"] = temperature
|
||
options["top_p"] = top_p
|
||
options["num_predict"] = max_tokens
|
||
|
||
payload["options"] = options
|
||
|
||
# 添加思维链控制参数(如 think: true 或 think: false)
|
||
if _thinking_extra:
|
||
payload.update(_thinking_extra)
|
||
|
||
from ..server import is_streaming_progress_enabled
|
||
|
||
# 动态超时计算: 基础30s + 每1000个Token预估增加5秒
|
||
estimated_timeout = 30.0 + (num_ctx / 1000) * 5.0
|
||
final_timeout = min(600.0, max(60.0, estimated_timeout)) # 限制在 60s - 600s 之间
|
||
|
||
# 创建统一进度条(自动处理等待→生成→完成的完整生命周期)
|
||
extra_info = f"Context:{num_ctx} | Timeout:{int(final_timeout)}s"
|
||
pbar = ProgressBar(
|
||
request_id=request_id,
|
||
service_name=provider_display_name,
|
||
extra_info=extra_info,
|
||
streaming=is_streaming_progress_enabled(),
|
||
task_type=task_type,
|
||
source=source
|
||
)
|
||
|
||
start_time = time.perf_counter()
|
||
|
||
try:
|
||
from .ollama_native import OllamaNativeAdapter
|
||
return await OllamaNativeAdapter.stream_chat(
|
||
model=model,
|
||
native_base=native_base,
|
||
payload=payload,
|
||
timeout=final_timeout,
|
||
pbar=pbar,
|
||
stream_callback=stream_callback,
|
||
cancel_event=cancel_event,
|
||
provider_label="Ollama",
|
||
include_reasoning=not filter_thinking_output,
|
||
)
|
||
finally:
|
||
if auto_unload:
|
||
try:
|
||
await cls._unload_ollama_model(model, {"base_url": native_base, "auto_unload": True})
|
||
except:
|
||
pass
|
||
|
||
# 关键修复:单独捕获外层 CancelledError,确保 pbar 被正确停止
|
||
except asyncio.CancelledError:
|
||
if 'pbar' in locals() and pbar:
|
||
pbar.cancel(f"{WARN_PREFIX} 任务被外部取消 | 服务:Ollama")
|
||
return {"success": False, "error": "任务被取消", "interrupted": True}
|
||
|
||
except Exception as e:
|
||
# 关键修复:确保 pbar 在异常时也被停止
|
||
if 'pbar' in locals() and pbar:
|
||
pbar.error(format_api_error(e, provider_display_name))
|
||
return {"success": False, "error": format_api_error(e, provider_display_name)}
|
||
|
||
@staticmethod
|
||
async def expand_prompt(
|
||
prompt: str,
|
||
request_id: Optional[str] = None,
|
||
stream_callback: Optional[Callable[[str], None]] = None,
|
||
custom_provider: Optional[str] = None,
|
||
custom_provider_config: Optional[Dict[str, Any]] = None,
|
||
system_message_override: Optional[Dict[str, str]] = None,
|
||
cancel_event: Optional[Any] = None,
|
||
task_type: str = None,
|
||
source: Optional[str] = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
使用大语言模型扩写提示词
|
||
|
||
参数:
|
||
prompt: 要扩写的提示词
|
||
request_id: 请求ID
|
||
stream_callback: 流式输出的回调函数
|
||
custom_provider: 自定义服务商
|
||
custom_provider_config: 自定义配置
|
||
system_message_override: 覆盖系统提示词
|
||
|
||
返回:
|
||
Dict: {"success": bool, "data": {"original": str, "expanded": str}, "error": str}
|
||
"""
|
||
try:
|
||
# 获取配置
|
||
if custom_provider and custom_provider_config:
|
||
provider = custom_provider
|
||
api_key = custom_provider_config.get('api_key')
|
||
model = custom_provider_config.get('model')
|
||
temperature = custom_provider_config.get('temperature', 0.7)
|
||
top_p = custom_provider_config.get('top_p', 0.9)
|
||
max_tokens = custom_provider_config.get('max_tokens', 2000)
|
||
base_url = custom_provider_config.get('base_url', '')
|
||
else:
|
||
config = LLMService._get_config()
|
||
provider = config.get('provider', 'unknown')
|
||
api_key = config.get('api_key')
|
||
model = config.get('model')
|
||
temperature = config.get('temperature', 0.7)
|
||
top_p = config.get('top_p', 0.9)
|
||
max_tokens = config.get('max_tokens', 2000)
|
||
base_url = config.get('base_url', '')
|
||
|
||
# 注:允许空API Key,支持无认证服务商(如deepinfra公开端点)
|
||
if not model:
|
||
return {"success": False, "error": "未配置模型名称"}
|
||
|
||
provider_display_name = LLMService.get_provider_display_name(provider)
|
||
|
||
|
||
|
||
from ..utils.common import REQUEST_PREFIX, PREFIX, format_model_with_thinking
|
||
|
||
# 获取系统提示词
|
||
if system_message_override and system_message_override.get('content'):
|
||
system_message = system_message_override
|
||
prompt_name = system_message.get('name', '节点自定义规则')
|
||
else:
|
||
from ..config_manager import config_manager
|
||
system_prompts = config_manager.get_system_prompts()
|
||
|
||
if not system_prompts or 'expand_prompts' not in system_prompts:
|
||
return {"success": False, "error": "提示词优化系统提示词加载失败"}
|
||
|
||
active_prompt_id = system_prompts.get('active_prompts', {}).get('expand', 'expand_default')
|
||
if active_prompt_id not in system_prompts['expand_prompts']:
|
||
if len(system_prompts['expand_prompts']) > 0:
|
||
active_prompt_id = list(system_prompts['expand_prompts'].keys())[0]
|
||
else:
|
||
return {"success": False, "error": "未找到可用的提示词优化系统提示词"}
|
||
|
||
system_message = system_prompts['expand_prompts'][active_prompt_id]
|
||
prompt_name = system_message.get('name', active_prompt_id)
|
||
|
||
# 检查服务配置的disable_thinking参数,只有开启时才关闭思维链
|
||
from ..config_manager import config_manager
|
||
service = config_manager.get_service(provider)
|
||
disable_thinking_enabled = service.get('disable_thinking', True) if service else True
|
||
|
||
# 始终调用 build_thinking_suppression,传递 disable_thinking 参数
|
||
_thinking_extra = build_thinking_suppression(service.get('type', provider) if service else provider, model, disable_thinking=disable_thinking_enabled)
|
||
thinking_disabled = _thinking_extra is not None and disable_thinking_enabled
|
||
model_display = format_model_with_thinking(model, thinking_disabled)
|
||
|
||
# 构建消息
|
||
lang_content = "请用中文回答" if LLMService._is_chinese(prompt) else "Please answer in English."
|
||
provider_type = service.get('type', provider) if service else provider
|
||
if should_append_no_thinking_instruction(provider_type, model, disable_thinking_enabled):
|
||
lang_content += " 请直接输出结果,不要包含任何思考过程、推理过程或 <think> 标签。"
|
||
|
||
lang_message = {
|
||
"role": "system",
|
||
"content": lang_content
|
||
}
|
||
messages = [lang_message, system_message, {"role": "user", "content": prompt}]
|
||
|
||
# 判断是否走原生 Ollama API:必须是 ollama 类型,且 base_url 不以 /v1 结尾或包含 /v1/
|
||
is_native_ollama = False
|
||
if service and service.get('type') == 'ollama':
|
||
# 兼容 "http://xxx:11434/v1/" 或 "http://xxx:11434/v1"
|
||
_url = base_url.rstrip('/')
|
||
if not _url.endswith('/v1') and '/v1/' not in base_url:
|
||
is_native_ollama = True
|
||
|
||
# Ollama走原生API
|
||
if is_native_ollama:
|
||
# 读取 Ollama 服务的配置
|
||
enable_advanced_params = service.get('enable_advanced_params', False)
|
||
filter_thinking_output = service.get('filter_thinking_output', True)
|
||
effective_filter_thinking_output = filter_thinking_output or disable_thinking_enabled
|
||
|
||
# 统一计算 native_base (确保移除 /v1 和末尾斜杠)
|
||
native_base = base_url.rstrip('/')
|
||
if native_base.endswith('/v1'):
|
||
native_base = native_base[:-3].rstrip('/')
|
||
|
||
# 再次兜底
|
||
if not native_base:
|
||
native_base = 'http://localhost:11434'
|
||
|
||
# 提前计算auto_unload配置
|
||
_cfg = {
|
||
'auto_unload': custom_provider_config.get('auto_unload', True) if custom_provider_config else config.get('auto_unload', True),
|
||
'base_url': native_base
|
||
}
|
||
auto_unload = _cfg['auto_unload']
|
||
|
||
result = await LLMService._call_ollama_native(
|
||
model=model,
|
||
messages=messages,
|
||
temperature=temperature,
|
||
top_p=top_p,
|
||
max_tokens=max_tokens,
|
||
base_url=base_url,
|
||
stream_callback=stream_callback,
|
||
request_id=request_id,
|
||
provider_display_name=provider_display_name,
|
||
auto_unload=auto_unload,
|
||
enable_advanced_params=enable_advanced_params,
|
||
thinking_extra=_thinking_extra,
|
||
filter_thinking_output=effective_filter_thinking_output,
|
||
cancel_event=cancel_event,
|
||
task_type=task_type or TASK_EXPAND,
|
||
source=source
|
||
)
|
||
|
||
if result["success"]:
|
||
success, content = postprocess_model_output(
|
||
result["content"],
|
||
filter_thinking_output=effective_filter_thinking_output,
|
||
)
|
||
|
||
# 最终检查内容是否为空
|
||
if not success:
|
||
return {"success": False, "error": "API returned empty result after filtering reasoning content (Ollama native)"}
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {"original": prompt, "expanded": content}
|
||
}
|
||
else:
|
||
return result
|
||
|
||
# 其他服务走HTTP直连
|
||
if not base_url:
|
||
base_url = LLMService.get_provider_base_url(provider, custom_provider_config if custom_provider else None)
|
||
|
||
# 检查disable_thinking、enable_advanced_params和filter_thinking_output配置
|
||
from ..config_manager import config_manager
|
||
service = config_manager.get_service(provider)
|
||
disable_thinking_enabled = service.get('disable_thinking', True) if service else True
|
||
enable_advanced_params = service.get('enable_advanced_params', False) if service else False
|
||
filter_thinking_output = service.get('filter_thinking_output', True) if service else True
|
||
effective_filter_thinking_output = filter_thinking_output or disable_thinking_enabled
|
||
thinking_extra = build_thinking_suppression(provider, model) if disable_thinking_enabled else None
|
||
|
||
result = await LLMService._http_request_chat_completions(
|
||
base_url=base_url,
|
||
api_key=api_key,
|
||
model=model,
|
||
messages=messages,
|
||
temperature=temperature,
|
||
top_p=top_p,
|
||
max_tokens=max_tokens,
|
||
thinking_extra=thinking_extra,
|
||
enable_advanced_params=enable_advanced_params,
|
||
stream_callback=stream_callback,
|
||
request_id=request_id,
|
||
provider_display_name=provider_display_name,
|
||
cancel_event=cancel_event,
|
||
task_type=task_type or TASK_EXPAND,
|
||
source=source,
|
||
filter_thinking_output=effective_filter_thinking_output
|
||
)
|
||
|
||
if result["success"]:
|
||
success, content = postprocess_model_output(
|
||
result["content"],
|
||
filter_thinking_output=effective_filter_thinking_output,
|
||
)
|
||
|
||
# 最终检查内容是否为空
|
||
if not success:
|
||
return {"success": False, "error": "API returned empty result after filtering reasoning content (Model only output thinking process)"}
|
||
return {
|
||
"success": True,
|
||
"data": {"original": prompt, "expanded": content}
|
||
}
|
||
else:
|
||
return result
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": format_api_error(e, "LLM服务")}
|
||
|
||
@staticmethod
|
||
async def translate(
|
||
text: str,
|
||
from_lang: str = 'auto',
|
||
to_lang: str = 'zh',
|
||
request_id: Optional[str] = None,
|
||
stream_callback: Optional[Callable[[str], None]] = None,
|
||
custom_provider: Optional[str] = None,
|
||
custom_provider_config: Optional[Dict[str, Any]] = None,
|
||
cancel_event: Optional[Any] = None,
|
||
task_type: str = None,
|
||
source: Optional[str] = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
使用大语言模型翻译文本
|
||
|
||
参数:
|
||
text: 要翻译的文本
|
||
from_lang: 源语言
|
||
to_lang: 目标语言
|
||
request_id: 请求ID
|
||
stream_callback: 流式输出回调
|
||
custom_provider: 自定义服务商
|
||
custom_provider_config: 自定义配置
|
||
|
||
返回:
|
||
Dict: {"success": bool, "data": {"original": str, "translated": str}, "error": str}
|
||
"""
|
||
try:
|
||
# 获取配置(翻译使用专门的翻译服务配置)
|
||
if custom_provider and custom_provider_config:
|
||
provider = custom_provider
|
||
api_key = custom_provider_config.get('api_key')
|
||
model = custom_provider_config.get('model')
|
||
temperature = custom_provider_config.get('temperature', 0.7)
|
||
top_p = custom_provider_config.get('top_p', 0.9)
|
||
max_tokens = custom_provider_config.get('max_tokens', 2000)
|
||
base_url = custom_provider_config.get('base_url', '')
|
||
else:
|
||
# 使用翻译服务配置(而非LLM配置)
|
||
from ..config_manager import config_manager
|
||
config = config_manager.get_translate_config()
|
||
provider = config.get('provider', 'unknown')
|
||
api_key = config.get('api_key')
|
||
model = config.get('model')
|
||
temperature = config.get('temperature', 0.7)
|
||
top_p = config.get('top_p', 0.9)
|
||
max_tokens = config.get('max_tokens', 2000)
|
||
base_url = config.get('base_url', '')
|
||
|
||
# 注:允许空API Key,支持无认证服务商
|
||
if not model:
|
||
return {"success": False, "error": "未配置模型名称"}
|
||
|
||
provider_display_name = LLMService.get_provider_display_name(provider)
|
||
|
||
from ..config_manager import config_manager
|
||
service = config_manager.get_service(provider)
|
||
|
||
from ..utils.common import REQUEST_PREFIX, PREFIX, format_model_with_thinking
|
||
|
||
# 检测是否关闭思维链
|
||
disable_thinking_enabled = service.get('disable_thinking', True) if service else True
|
||
_thinking_extra = build_thinking_suppression(provider, model) if disable_thinking_enabled else None
|
||
thinking_disabled = _thinking_extra is not None and disable_thinking_enabled
|
||
model_display = format_model_with_thinking(model, thinking_disabled)
|
||
|
||
# 正确修复(BUG-06): 从规则管理器读取用户自定义翻译提示词
|
||
# translate_prompts 配置中包含 {src_lang}/{dst_lang} 占位符,运行时动态替换
|
||
# 语言代码映射为自然语言名称,提升模型理解稳定性
|
||
_LANG_NAMES = {
|
||
"zh": "Simplified Chinese (简体中文)",
|
||
"zh-tw": "Traditional Chinese (繁體中文)",
|
||
"en": "English",
|
||
"ja": "Japanese (日本語)",
|
||
"ko": "Korean (한국어)",
|
||
"fr": "French",
|
||
"de": "German",
|
||
"es": "Spanish",
|
||
"ru": "Russian",
|
||
"ar": "Arabic",
|
||
"pt": "Portuguese",
|
||
"pt-br": "Brazilian Portuguese",
|
||
"it": "Italian",
|
||
"tr": "Turkish",
|
||
"fa": "Persian (Farsi)",
|
||
"auto": "the detected source language"
|
||
}
|
||
from_name = _LANG_NAMES.get(from_lang.lower(), from_lang)
|
||
to_name = _LANG_NAMES.get(to_lang.lower(), to_lang)
|
||
|
||
translate_instruction = None
|
||
try:
|
||
system_prompts_data = config_manager.get_system_prompts()
|
||
translate_prompts = system_prompts_data.get("translate_prompts", {})
|
||
# 读取激活的翻译规则(当前只有 ZH,后续可扩展)
|
||
active_prompts = system_prompts_data.get("active_prompts", {})
|
||
active_translate_id = active_prompts.get("translate", "ZH")
|
||
prompt_entry = translate_prompts.get(active_translate_id)
|
||
# 若激活的规则不存在,回退到第一个可用规则
|
||
if not prompt_entry and translate_prompts:
|
||
prompt_entry = next(iter(translate_prompts.values()))
|
||
if prompt_entry and prompt_entry.get("content"):
|
||
# 替换 {src_lang} / {dst_lang} 占位符为自然语言名称
|
||
translate_instruction = (
|
||
prompt_entry["content"]
|
||
.replace("{src_lang}", from_name)
|
||
.replace("{dst_lang}", to_name)
|
||
)
|
||
except Exception as e:
|
||
print(f"\n[PA] 读取翻译规则失败,使用默认提示词: {e}", flush=True)
|
||
|
||
# 终极兜底:规则管理器读取失败时使用英文简化指令
|
||
if not translate_instruction:
|
||
translate_instruction = (
|
||
f"Translate the following text from {from_name} to {to_name}. "
|
||
f"Output only the translation result with no explanations or additional content."
|
||
)
|
||
|
||
messages = [
|
||
{"role": "system", "content": translate_instruction},
|
||
{"role": "user", "content": text}
|
||
]
|
||
|
||
|
||
# 判断是否走原生 Ollama API:必须是 ollama 类型,且 base_url 不以 /v1 结尾或包含 /v1/
|
||
is_native_ollama = False
|
||
if service and service.get('type') == 'ollama':
|
||
# 兼容 "http://xxx:11434/v1/" 或 "http://xxx:11434/v1"
|
||
_url = base_url.rstrip('/')
|
||
if not _url.endswith('/v1') and '/v1/' not in base_url:
|
||
is_native_ollama = True
|
||
|
||
# Ollama走原生API
|
||
if is_native_ollama:
|
||
# 读取 Ollama 服务的配置
|
||
disable_thinking_enabled = service.get('disable_thinking', True)
|
||
enable_advanced_params = service.get('enable_advanced_params', False)
|
||
filter_thinking_output = service.get('filter_thinking_output', True)
|
||
effective_filter_thinking_output = filter_thinking_output or disable_thinking_enabled
|
||
_ollama_thinking_extra = build_thinking_suppression(service.get('type', provider) if service else provider, model) if disable_thinking_enabled else None
|
||
|
||
# 统一计算 native_base (确保移除 /v1 和末尾斜杠)
|
||
native_base = base_url.rstrip('/')
|
||
if native_base.endswith('/v1'):
|
||
native_base = native_base[:-3].rstrip('/')
|
||
|
||
# 再次兜底
|
||
if not native_base:
|
||
native_base = 'http://localhost:11434'
|
||
|
||
_cfg = {
|
||
'auto_unload': custom_provider_config.get('auto_unload', True) if custom_provider_config else config.get('auto_unload', True),
|
||
'base_url': native_base
|
||
}
|
||
auto_unload = _cfg['auto_unload']
|
||
|
||
result = await LLMService._call_ollama_native(
|
||
model=model,
|
||
messages=[{"role": "system", "content": translate_instruction}, {"role": "user", "content": text}],
|
||
temperature=temperature,
|
||
top_p=top_p,
|
||
max_tokens=max_tokens,
|
||
base_url=base_url,
|
||
stream_callback=stream_callback,
|
||
request_id=request_id,
|
||
provider_display_name=provider_display_name,
|
||
auto_unload=auto_unload,
|
||
enable_advanced_params=enable_advanced_params,
|
||
thinking_extra=_ollama_thinking_extra,
|
||
filter_thinking_output=effective_filter_thinking_output,
|
||
cancel_event=cancel_event,
|
||
task_type=task_type or TASK_TRANSLATE,
|
||
source=source
|
||
)
|
||
|
||
if result["success"]:
|
||
success, content = postprocess_model_output(
|
||
result["content"],
|
||
filter_thinking_output=effective_filter_thinking_output,
|
||
)
|
||
|
||
# 最终检查
|
||
if not success:
|
||
return {"success": False, "error": "API returned empty result after filtering reasoning content (Ollama native)"}
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {"original": text, "translated": content}
|
||
}
|
||
else:
|
||
return result
|
||
|
||
# 其他服务走HTTP直连
|
||
if not base_url:
|
||
base_url = LLMService.get_provider_base_url(provider, custom_provider_config if custom_provider else None)
|
||
|
||
# 检查enable_advanced_params和filter_thinking_output配置
|
||
enable_advanced_params = service.get('enable_advanced_params', False) if service else False
|
||
filter_thinking_output = service.get('filter_thinking_output', True) if service else True
|
||
effective_filter_thinking_output = filter_thinking_output or disable_thinking_enabled
|
||
thinking_extra = _thinking_extra # 复用前面计算好的 suppression
|
||
|
||
result = await LLMService._http_request_chat_completions(
|
||
base_url=base_url,
|
||
api_key=api_key,
|
||
model=model,
|
||
messages=messages,
|
||
temperature=temperature,
|
||
top_p=top_p,
|
||
max_tokens=max_tokens,
|
||
thinking_extra=thinking_extra,
|
||
enable_advanced_params=enable_advanced_params,
|
||
stream_callback=stream_callback,
|
||
request_id=request_id,
|
||
provider_display_name=provider_display_name,
|
||
cancel_event=cancel_event,
|
||
task_type=task_type or TASK_TRANSLATE,
|
||
source=source,
|
||
filter_thinking_output=effective_filter_thinking_output
|
||
)
|
||
|
||
if result["success"]:
|
||
success, content = postprocess_model_output(
|
||
result["content"],
|
||
filter_thinking_output=effective_filter_thinking_output,
|
||
)
|
||
|
||
# 最终检查
|
||
if not success:
|
||
return {"success": False, "error": "API returned empty result after filtering reasoning content"}
|
||
return {
|
||
"success": True,
|
||
"data": {"original": text, "translated": content}
|
||
}
|
||
else:
|
||
return result
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": format_api_error(e, "LLM服务")}
|