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}";
|
||||
|
||||
/**
|
||||
* 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
|
||||
public String generateSummary(String conversation) {
|
||||
return generateSummary(conversation, null, null);
|
||||
@@ -101,6 +122,9 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
||||
String baseUrl = configJson.getStr("base_url");
|
||||
String model = configJson.getStr("model_name");
|
||||
String apiKey = configJson.getStr("api_key");
|
||||
if ("ollama".equalsIgnoreCase(configJson.getStr("type"))) {
|
||||
baseUrl = normalizeOllamaBaseUrl(baseUrl);
|
||||
}
|
||||
Double temperature = configJson.getDouble("temperature");
|
||||
Integer maxTokens = configJson.getInt("max_tokens");
|
||||
|
||||
@@ -196,6 +220,9 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
||||
String baseUrl = configJson.getStr("base_url");
|
||||
String model = configJson.getStr("model_name");
|
||||
String apiKey = configJson.getStr("api_key");
|
||||
if ("ollama".equalsIgnoreCase(configJson.getStr("type"))) {
|
||||
baseUrl = normalizeOllamaBaseUrl(baseUrl);
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(baseUrl) || StringUtils.isBlank(apiKey)) {
|
||||
log.error("LLM配置不完整,baseUrl或apiKey为空");
|
||||
@@ -354,6 +381,9 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
|
||||
String baseUrl = configJson.getStr("base_url");
|
||||
String model = configJson.getStr("model_name");
|
||||
String apiKey = configJson.getStr("api_key");
|
||||
if ("ollama".equalsIgnoreCase(configJson.getStr("type"))) {
|
||||
baseUrl = normalizeOllamaBaseUrl(baseUrl);
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(baseUrl) || StringUtils.isBlank(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:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202609172100.sql
|
||||
- changeSet:
|
||||
id: 202610291451
|
||||
author: RanChen
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202610291451.sql
|
||||
- changeSet:
|
||||
id: 202610302051
|
||||
author: shinjiyu
|
||||
|
||||
@@ -666,6 +666,7 @@ LLM:
|
||||
type: ollama
|
||||
model_name: qwen2.5 # 使用的模型名称,需要预先使用ollama pull下载
|
||||
base_url: http://localhost:11434 # Ollama服务地址
|
||||
api_key: ollama # 本地部署无需真实密钥,占位即可;走鉴权网关或Ollama Cloud时修改为真实key
|
||||
DifyLLM:
|
||||
# 定义LLM API类型
|
||||
type: dify
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from config.logger import setup_logging
|
||||
from openai import OpenAI
|
||||
from urllib.parse import urlsplit
|
||||
import json
|
||||
from core.providers.llm.base import LLMProviderBase
|
||||
|
||||
@@ -10,15 +11,20 @@ logger = setup_logging()
|
||||
class LLMProvider(LLMProviderBase):
|
||||
def __init__(self, config):
|
||||
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
|
||||
# 如果没有v1,增加v1
|
||||
if not self.base_url.endswith("/v1"):
|
||||
# 纯主机地址补/v1(Ollama默认约定);带路径的地址尊重用户输入,不再改写
|
||||
if not urlsplit(self.base_url).path.strip("/"):
|
||||
self.base_url = f"{self.base_url}/v1"
|
||||
|
||||
self.client = OpenAI(
|
||||
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模型
|
||||
|
||||
@@ -119,7 +119,12 @@ class LLMPerformanceTester:
|
||||
return chunks, first_token_time
|
||||
|
||||
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:
|
||||
try:
|
||||
async with session.get(f"{base_url}/api/version") as response:
|
||||
@@ -230,7 +235,7 @@ class LLMPerformanceTester:
|
||||
"""异步测试单个 LLM 性能"""
|
||||
try:
|
||||
# 对于 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")
|
||||
model_name = config.get("model_name")
|
||||
if not model_name:
|
||||
@@ -474,7 +479,7 @@ class LLMPerformanceTester:
|
||||
continue
|
||||
|
||||
# 对于 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")
|
||||
model_name = config.get("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:
|
||||
"""任一候选 key 名下值不是占位符就算有真密钥。
|
||||
|
||||
占位符判定:空 / "你的..." / 包含 "placeholder" / 字面 "none"。
|
||||
占位符判定:空 / "你的..." / 包含 "placeholder" / 字面 "none" / "ollama"
|
||||
(Ollama本地服务无需鉴权,config.yaml默认占位值即为"ollama")。
|
||||
"""
|
||||
if not config:
|
||||
return False
|
||||
@@ -193,7 +194,7 @@ def has_real_key(config: dict, *keys: str) -> bool:
|
||||
if val is None:
|
||||
continue
|
||||
s = str(val).strip().lower()
|
||||
if not s or s == "none":
|
||||
if not s or s == "none" or s == "ollama":
|
||||
continue
|
||||
if s.startswith("你的") or "placeholder" in s:
|
||||
continue
|
||||
@@ -390,6 +391,9 @@ def _check_modules_ready() -> None:
|
||||
f"{module}.{provider_name} 配置缺失(CFG 字典里没这个 provider)"
|
||||
)
|
||||
continue
|
||||
# Ollama免鉴权供应器无需真实key,跳过占位符检查
|
||||
if str(cfg.get("type", "")).lower() == "ollama":
|
||||
continue
|
||||
secret_keys_present = [k for k in _SECRET_FIELDS if k in cfg]
|
||||
if secret_keys_present and not any(
|
||||
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 ["你的"]
|
||||
):
|
||||
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"):
|
||||
if LIVE_API_TESTS:
|
||||
pytest.skip(f"LLM '{name}' 的 api_key 未配置")
|
||||
|
||||
Reference in new issue
Block a user