Compare commits
5
Commits
main
...
py-test-ollama
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e09df59c13 | ||
|
|
7f0d416e66 | ||
|
|
c282397cd2 | ||
|
|
5f6e191d11 | ||
|
|
27833f918c |
No files matched your search
+30
@@ -64,6 +64,27 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
|||||||
|
|
||||||
private static final String DEFAULT_TITLE_PROMPT = "请根据以下对话内容,生成一个简洁的会话标题(约15字以内),只返回标题,不要包含任何解释或标点符号:\n{conversation}";
|
private static final String DEFAULT_TITLE_PROMPT = "请根据以下对话内容,生成一个简洁的会话标题(约15字以内),只返回标题,不要包含任何解释或标点符号:\n{conversation}";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ollama的OpenAI兼容端点位于{base_url}/v1下,与xiaozhi-server侧ollama provider逻辑保持一致:
|
||||||
|
* 剥离用户误填的完整端点后缀后,仅对不含路径的纯主机地址补/v1
|
||||||
|
*/
|
||||||
|
private static String normalizeOllamaBaseUrl(String baseUrl) {
|
||||||
|
if (baseUrl == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String url = baseUrl.trim();
|
||||||
|
if (url.endsWith("/chat/completions")) {
|
||||||
|
url = url.substring(0, url.length() - "/chat/completions".length());
|
||||||
|
}
|
||||||
|
while (url.endsWith("/")) {
|
||||||
|
url = url.substring(0, url.length() - 1);
|
||||||
|
}
|
||||||
|
if (url.replaceFirst("^https?://[^/]+", "").isEmpty()) {
|
||||||
|
url = url + "/v1";
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String generateSummary(String conversation) {
|
public String generateSummary(String conversation) {
|
||||||
return generateSummary(conversation, null, null);
|
return generateSummary(conversation, null, null);
|
||||||
@@ -101,6 +122,9 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
|||||||
String baseUrl = configJson.getStr("base_url");
|
String baseUrl = configJson.getStr("base_url");
|
||||||
String model = configJson.getStr("model_name");
|
String model = configJson.getStr("model_name");
|
||||||
String apiKey = configJson.getStr("api_key");
|
String apiKey = configJson.getStr("api_key");
|
||||||
|
if ("ollama".equalsIgnoreCase(configJson.getStr("type"))) {
|
||||||
|
baseUrl = normalizeOllamaBaseUrl(baseUrl);
|
||||||
|
}
|
||||||
Double temperature = configJson.getDouble("temperature");
|
Double temperature = configJson.getDouble("temperature");
|
||||||
Integer maxTokens = configJson.getInt("max_tokens");
|
Integer maxTokens = configJson.getInt("max_tokens");
|
||||||
|
|
||||||
@@ -196,6 +220,9 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
|||||||
String baseUrl = configJson.getStr("base_url");
|
String baseUrl = configJson.getStr("base_url");
|
||||||
String model = configJson.getStr("model_name");
|
String model = configJson.getStr("model_name");
|
||||||
String apiKey = configJson.getStr("api_key");
|
String apiKey = configJson.getStr("api_key");
|
||||||
|
if ("ollama".equalsIgnoreCase(configJson.getStr("type"))) {
|
||||||
|
baseUrl = normalizeOllamaBaseUrl(baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
if (StringUtils.isBlank(baseUrl) || StringUtils.isBlank(apiKey)) {
|
if (StringUtils.isBlank(baseUrl) || StringUtils.isBlank(apiKey)) {
|
||||||
log.error("LLM配置不完整,baseUrl或apiKey为空");
|
log.error("LLM配置不完整,baseUrl或apiKey为空");
|
||||||
@@ -354,6 +381,9 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
|||||||
String baseUrl = configJson.getStr("base_url");
|
String baseUrl = configJson.getStr("base_url");
|
||||||
String model = configJson.getStr("model_name");
|
String model = configJson.getStr("model_name");
|
||||||
String apiKey = configJson.getStr("api_key");
|
String apiKey = configJson.getStr("api_key");
|
||||||
|
if ("ollama".equalsIgnoreCase(configJson.getStr("type"))) {
|
||||||
|
baseUrl = normalizeOllamaBaseUrl(baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
if (StringUtils.isBlank(baseUrl) || StringUtils.isBlank(apiKey)) {
|
if (StringUtils.isBlank(baseUrl) || StringUtils.isBlank(apiKey)) {
|
||||||
log.error("LLM配置不完整,baseUrl或apiKey为空");
|
log.error("LLM配置不完整,baseUrl或apiKey为空");
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- Ollama LLM配置补充api_key占位值,走鉴权网关/Ollama Cloud时使用
|
||||||
|
UPDATE `ai_model_config`
|
||||||
|
SET `config_json` = JSON_SET(`config_json`, '$.api_key', 'ollama')
|
||||||
|
WHERE `id` = 'LLM_OllamaLLM' AND JSON_EXTRACT(`config_json`, '$.api_key') IS NULL;
|
||||||
|
|
||||||
|
-- Ollama供应商追加api_key字段定义,password类型
|
||||||
|
UPDATE `ai_model_provider`
|
||||||
|
SET `fields` = JSON_ARRAY_APPEND(COALESCE(`fields`, JSON_ARRAY()), '$',
|
||||||
|
JSON_OBJECT('key', 'api_key', 'label', 'API密钥', 'type', 'password'))
|
||||||
|
WHERE `id` = 'SYSTEM_LLM_ollama'
|
||||||
|
AND JSON_SEARCH(`fields`, 'one', 'api_key', NULL, '$[*].key') IS NULL;
|
||||||
|
|
||||||
|
-- 配置说明补充api_key条目
|
||||||
|
UPDATE `ai_model_config`
|
||||||
|
SET `remark` = CONCAT(`remark`, '\n4. 走鉴权网关或使用Ollama Cloud时,需填写api_key')
|
||||||
|
WHERE `id` = 'LLM_OllamaLLM'
|
||||||
|
AND `remark` IS NOT NULL AND `remark` NOT LIKE '%api_key%';
|
||||||
@@ -753,6 +753,13 @@ databaseChangeLog:
|
|||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202609172100.sql
|
path: classpath:db/changelog/202609172100.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202610291451
|
||||||
|
author: RanChen
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202610291451.sql
|
||||||
- changeSet:
|
- changeSet:
|
||||||
id: 202610302051
|
id: 202610302051
|
||||||
author: shinjiyu
|
author: shinjiyu
|
||||||
|
|||||||
@@ -666,6 +666,7 @@ LLM:
|
|||||||
type: ollama
|
type: ollama
|
||||||
model_name: qwen2.5 # 使用的模型名称,需要预先使用ollama pull下载
|
model_name: qwen2.5 # 使用的模型名称,需要预先使用ollama pull下载
|
||||||
base_url: http://localhost:11434 # Ollama服务地址
|
base_url: http://localhost:11434 # Ollama服务地址
|
||||||
|
api_key: ollama # 本地部署无需真实密钥,占位即可;走鉴权网关或Ollama Cloud时修改为真实key
|
||||||
DifyLLM:
|
DifyLLM:
|
||||||
# 定义LLM API类型
|
# 定义LLM API类型
|
||||||
type: dify
|
type: dify
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
|
from urllib.parse import urlsplit
|
||||||
import json
|
import json
|
||||||
from core.providers.llm.base import LLMProviderBase
|
from core.providers.llm.base import LLMProviderBase
|
||||||
|
|
||||||
@@ -10,15 +11,20 @@ logger = setup_logging()
|
|||||||
class LLMProvider(LLMProviderBase):
|
class LLMProvider(LLMProviderBase):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.model_name = config.get("model_name")
|
self.model_name = config.get("model_name")
|
||||||
self.base_url = config.get("base_url", "http://localhost:11434")
|
self.base_url = (
|
||||||
|
str(config.get("base_url") or "").strip() or "http://localhost:11434"
|
||||||
|
).rstrip("/")
|
||||||
|
# 兼容处理用户输入完整端点的情况
|
||||||
|
if self.base_url.endswith("/chat/completions"):
|
||||||
|
self.base_url = self.base_url[:-len("/chat/completions")].rstrip("/")
|
||||||
# Initialize OpenAI client with Ollama base URL
|
# Initialize OpenAI client with Ollama base URL
|
||||||
# 如果没有v1,增加v1
|
# 纯主机地址补/v1(Ollama默认约定);带路径的地址尊重用户输入,不再改写
|
||||||
if not self.base_url.endswith("/v1"):
|
if not urlsplit(self.base_url).path.strip("/"):
|
||||||
self.base_url = f"{self.base_url}/v1"
|
self.base_url = f"{self.base_url}/v1"
|
||||||
|
|
||||||
self.client = OpenAI(
|
self.client = OpenAI(
|
||||||
base_url=self.base_url,
|
base_url=self.base_url,
|
||||||
api_key="ollama", # Ollama doesn't need an API key but OpenAI client requires one
|
api_key=config.get("api_key") or "ollama",
|
||||||
)
|
)
|
||||||
|
|
||||||
# 检查是否是qwen3模型
|
# 检查是否是qwen3模型
|
||||||
|
|||||||
@@ -119,7 +119,12 @@ class LLMPerformanceTester:
|
|||||||
return chunks, first_token_time
|
return chunks, first_token_time
|
||||||
|
|
||||||
async def _check_ollama_service(self, base_url: str, model_name: str) -> bool:
|
async def _check_ollama_service(self, base_url: str, model_name: str) -> bool:
|
||||||
"""异步检查 Ollama 服务状态"""
|
"""异步检查 Ollama 服务状态(兼容带/v1或完整端点的base_url写法)"""
|
||||||
|
base_url = base_url.strip().rstrip("/")
|
||||||
|
if base_url.endswith("/chat/completions"):
|
||||||
|
base_url = base_url[: -len("/chat/completions")].rstrip("/")
|
||||||
|
if base_url.endswith("/v1"):
|
||||||
|
base_url = base_url[: -len("/v1")].rstrip("/")
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
try:
|
try:
|
||||||
async with session.get(f"{base_url}/api/version") as response:
|
async with session.get(f"{base_url}/api/version") as response:
|
||||||
@@ -230,7 +235,7 @@ class LLMPerformanceTester:
|
|||||||
"""异步测试单个 LLM 性能"""
|
"""异步测试单个 LLM 性能"""
|
||||||
try:
|
try:
|
||||||
# 对于 Ollama,跳过 api_key 检查并进行特殊处理
|
# 对于 Ollama,跳过 api_key 检查并进行特殊处理
|
||||||
if llm_name == "Ollama":
|
if llm_name == "OllamaLLM" or str(config.get("type", "")).lower() == "ollama":
|
||||||
base_url = config.get("base_url", "http://localhost:11434")
|
base_url = config.get("base_url", "http://localhost:11434")
|
||||||
model_name = config.get("model_name")
|
model_name = config.get("model_name")
|
||||||
if not model_name:
|
if not model_name:
|
||||||
@@ -474,7 +479,7 @@ class LLMPerformanceTester:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# 对于 Ollama,先检查服务状态
|
# 对于 Ollama,先检查服务状态
|
||||||
if llm_name == "Ollama":
|
if llm_name == "OllamaLLM" or str(config.get("type", "")).lower() == "ollama":
|
||||||
base_url = config.get("base_url", "http://localhost:11434")
|
base_url = config.get("base_url", "http://localhost:11434")
|
||||||
model_name = config.get("model_name")
|
model_name = config.get("model_name")
|
||||||
if not model_name:
|
if not model_name:
|
||||||
|
|||||||
@@ -184,7 +184,8 @@ def get_provider_type(module: str, provider_name: str | None = None) -> str | No
|
|||||||
def has_real_key(config: dict, *keys: str) -> bool:
|
def has_real_key(config: dict, *keys: str) -> bool:
|
||||||
"""任一候选 key 名下值不是占位符就算有真密钥。
|
"""任一候选 key 名下值不是占位符就算有真密钥。
|
||||||
|
|
||||||
占位符判定:空 / "你的..." / 包含 "placeholder" / 字面 "none"。
|
占位符判定:空 / "你的..." / 包含 "placeholder" / 字面 "none" / "ollama"
|
||||||
|
(Ollama本地服务无需鉴权,config.yaml默认占位值即为"ollama")。
|
||||||
"""
|
"""
|
||||||
if not config:
|
if not config:
|
||||||
return False
|
return False
|
||||||
@@ -193,7 +194,7 @@ def has_real_key(config: dict, *keys: str) -> bool:
|
|||||||
if val is None:
|
if val is None:
|
||||||
continue
|
continue
|
||||||
s = str(val).strip().lower()
|
s = str(val).strip().lower()
|
||||||
if not s or s == "none":
|
if not s or s == "none" or s == "ollama":
|
||||||
continue
|
continue
|
||||||
if s.startswith("你的") or "placeholder" in s:
|
if s.startswith("你的") or "placeholder" in s:
|
||||||
continue
|
continue
|
||||||
@@ -390,6 +391,9 @@ def _check_modules_ready() -> None:
|
|||||||
f"{module}.{provider_name} 配置缺失(CFG 字典里没这个 provider)"
|
f"{module}.{provider_name} 配置缺失(CFG 字典里没这个 provider)"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
# Ollama免鉴权供应器无需真实key,跳过占位符检查
|
||||||
|
if str(cfg.get("type", "")).lower() == "ollama":
|
||||||
|
continue
|
||||||
secret_keys_present = [k for k in _SECRET_FIELDS if k in cfg]
|
secret_keys_present = [k for k in _SECRET_FIELDS if k in cfg]
|
||||||
if secret_keys_present and not any(
|
if secret_keys_present and not any(
|
||||||
has_real_key(cfg, k) for k in secret_keys_present
|
has_real_key(cfg, k) for k in secret_keys_present
|
||||||
|
|||||||
@@ -50,9 +50,6 @@ def _build_provider():
|
|||||||
x in cfg.get("user_id", "") for x in ["你的"]
|
x in cfg.get("user_id", "") for x in ["你的"]
|
||||||
):
|
):
|
||||||
pytest.skip("LLM 'CozeLLM' 的 bot_id/user_id 未配置")
|
pytest.skip("LLM 'CozeLLM' 的 bot_id/user_id 未配置")
|
||||||
elif name == "Ollama":
|
|
||||||
if not cfg.get("model_name"):
|
|
||||||
pytest.skip("LLM 'Ollama' 的 model_name 未配置")
|
|
||||||
elif not has_real_key(cfg, "api_key"):
|
elif not has_real_key(cfg, "api_key"):
|
||||||
if LIVE_API_TESTS:
|
if LIVE_API_TESTS:
|
||||||
pytest.skip(f"LLM '{name}' 的 api_key 未配置")
|
pytest.skip(f"LLM '{name}' 的 api_key 未配置")
|
||||||
|
|||||||
Reference in new issue
Block a user