diff --git a/conf/mod_ollama_chat.conf.dist b/conf/mod_ollama_chat.conf.dist index b6000322e..520ab61c3 100644 --- a/conf/mod_ollama_chat.conf.dist +++ b/conf/mod_ollama_chat.conf.dist @@ -43,6 +43,10 @@ OllamaChat.Url = http://localhost:11434/api/generate # Default: llama3.2:1b OllamaChat.Model = llama3.2:1b +# OllamaChat.ApiKey — optional Bearer token for OllamaChat.Url +# Default: (empty) +OllamaChat.ApiKey = + # OllamaChat.NumPredict # Description: Maximum number of tokens to generate in Ollama responses. # 0 = unlimited. Only set if you want a hard cap. diff --git a/src/mod-ollama-chat_api.cpp b/src/mod-ollama-chat_api.cpp index 271077d9d..884c9b71c 100644 --- a/src/mod-ollama-chat_api.cpp +++ b/src/mod-ollama-chat_api.cpp @@ -4,12 +4,15 @@ #include "mod-ollama-chat-utilities.h" #include "Log.h" #include +#include #include #include #include #include #include #include +#include +#include std::string ExtractTextBetweenDoubleQuotes(const std::string& response) { @@ -21,6 +24,61 @@ std::string ExtractTextBetweenDoubleQuotes(const std::string& response) return response; } +static bool IsOpenAIChatCompletionsUrl(std::string url) +{ + for (char& c : url) + c = static_cast(::tolower(static_cast(c))); + + // OpenAI-compatible chat completions endpoint + return url.find("/v1/chat/completions") != std::string::npos || url.find("/chat/completions") != std::string::npos; +} + +static std::string ExtractOpenAIChoiceText(nlohmann::json const& choice) +{ + if (choice.contains("message") && choice["message"].is_object()) + { + nlohmann::json const& msg = choice["message"]; + if (msg.contains("content")) + { + nlohmann::json const& content = msg["content"]; + if (content.is_string()) + { + std::string s = content.get(); + if (!s.empty()) + return s; + } + else if (content.is_array()) + { + std::ostringstream out; + for (nlohmann::json const& part : content) + { + if (part.is_string()) + out << part.get(); + else if (part.is_object()) + { + if (part.contains("text") && part["text"].is_string()) + out << part["text"].get(); + else if (part.contains("content") && part["content"].is_string()) + out << part["content"].get(); + } + } + std::string s = out.str(); + if (!s.empty()) + return s; + } + } + if (msg.contains("reasoning") && msg["reasoning"].is_string()) + { + std::string s = msg["reasoning"].get(); + if (!s.empty()) + return s; + } + } + if (choice.contains("text") && choice["text"].is_string()) + return choice["text"].get(); + return ""; +} + // Function to perform the API call. std::string QueryOllamaAPI(const std::string& prompt) { @@ -43,6 +101,103 @@ std::string QueryOllamaAPI(const std::string& prompt) // Sanitize the prompt to ensure it's valid UTF-8 before creating JSON std::string sanitizedPrompt = SanitizeUTF8(prompt); + if (IsOpenAIChatCompletionsUrl(url)) + { + nlohmann::json messages = nlohmann::json::array(); + if (!g_OllamaSystemPrompt.empty()) + messages.push_back(nlohmann::json{{"role", "system"}, {"content", SanitizeUTF8(g_OllamaSystemPrompt)}}); + messages.push_back(nlohmann::json{{"role", "user"}, {"content", sanitizedPrompt}}); + + nlohmann::json requestData = { + {"model", model}, + {"messages", messages}, + {"stream", false} + }; + + if (g_OllamaNumPredict > 0) + requestData["max_tokens"] = g_OllamaNumPredict; + if (g_OllamaTemperature != 0.8f) + requestData["temperature"] = g_OllamaTemperature; + if (g_OllamaTopP != 0.95f) + requestData["top_p"] = g_OllamaTopP; + + if (!g_OllamaStop.empty()) { + std::vector stopSeqs; + std::stringstream ss(g_OllamaStop); + std::string item; + while (std::getline(ss, item, ',')) { + size_t start = item.find_first_not_of(" \t"); + size_t end = item.find_last_not_of(" \t"); + if (start != std::string::npos && end != std::string::npos) + stopSeqs.push_back(item.substr(start, end - start + 1)); + } + if (!stopSeqs.empty()) + requestData["stop"] = stopSeqs; + } + + std::string requestDataStr = requestData.dump(); + std::string responseBuffer = httpClient.Post(url, requestDataStr, g_OllamaApiKey); + + if (responseBuffer.empty()) + { + LOG_ERROR("server.loading", "[OllamaChat] ERROR: Failed to reach LLM API at {}. Check URL configuration and network connectivity.", url); + if(g_DebugEnabled) + { + LOG_INFO("server.loading", "[OllamaChat] Debug: Empty response buffer from HTTP client. Model: {}", model); + } + return ""; + } + + std::string botReply; + try + { + nlohmann::json const root = nlohmann::json::parse(responseBuffer); + if (root.contains("error")) + { + LOG_ERROR("server.loading", "[OllamaChat] ERROR: OpenAI-compatible API error: {}", root["error"].dump()); + return ""; + } + if (root.contains("choices") && root["choices"].is_array() && !root["choices"].empty()) + botReply = ExtractOpenAIChoiceText(root["choices"][0]); + } + catch (const std::exception& e) + { + LOG_ERROR("server.loading", "[OllamaChat] ERROR: JSON parsing failed. Exception: {}", e.what()); + if(g_DebugEnabled) + { + LOG_INFO("server.loading", "[OllamaChat] Debug: Response buffer content: {}", responseBuffer); + } + return ""; + } + + if (botReply.empty()) + { + LOG_ERROR("server.loading", "[OllamaChat] ERROR: Empty response extracted from API. Model may not have generated any output."); + if (g_DebugEnabled) + { + try + { + auto dbg = nlohmann::json::parse(responseBuffer); + if (dbg.contains("choices") && dbg["choices"].is_array() && !dbg["choices"].empty() + && dbg["choices"][0].contains("finish_reason")) + LOG_INFO("server.loading", "[OllamaChat] Debug: finish_reason={}", + dbg["choices"][0]["finish_reason"].dump()); + } + catch (...) { } + LOG_INFO("server.loading", "[OllamaChat] Debug: response body (truncated): {}", + responseBuffer.substr(0, 500)); + } + return ""; + } + + if(g_DebugEnabled) + { + LOG_INFO("server.loading", "[Ollama Chat] Parsed bot response: {}", botReply); + } + + return botReply; + } + nlohmann::json requestData = { {"model", model}, {"prompt", sanitizedPrompt}, @@ -104,9 +259,9 @@ std::string QueryOllamaAPI(const std::string& prompt) if (!g_OllamaStop.empty()) { // If comma-separated, convert to array std::vector stopSeqs; - std::stringstream ss(g_OllamaStop); + std::stringstream stopStream(g_OllamaStop); std::string item; - while (std::getline(ss, item, ',')) { + while (std::getline(stopStream, item, ',')) { // trim whitespace size_t start = item.find_first_not_of(" \t"); size_t end = item.find_last_not_of(" \t"); @@ -135,11 +290,11 @@ std::string QueryOllamaAPI(const std::string& prompt) std::string requestDataStr = requestData.dump(); // Make HTTP POST request using our custom client - std::string responseBuffer = httpClient.Post(url, requestDataStr); + std::string responseBuffer = httpClient.Post(url, requestDataStr, g_OllamaApiKey); if (responseBuffer.empty()) { - LOG_ERROR("server.loading", "[OllamaChat] ERROR: Failed to reach Ollama API at {}. Check URL configuration and network connectivity.", url); + LOG_ERROR("server.loading", "[OllamaChat] ERROR: Failed to reach LLM API at {}. Check URL configuration and network connectivity.", url); if(g_DebugEnabled) { LOG_INFO("server.loading", "[OllamaChat] Debug: Empty response buffer from HTTP client. Model: {}", model); @@ -155,7 +310,7 @@ std::string QueryOllamaAPI(const std::string& prompt) { while (std::getline(ss, line)) { - if (line.empty() || std::all_of(line.begin(), line.end(), isspace)) + if (line.empty() || std::all_of(line.begin(), line.end(), [](unsigned char ch) { return std::isspace(ch); })) continue; nlohmann::json jsonResponse = nlohmann::json::parse(line); diff --git a/src/mod-ollama-chat_config.cpp b/src/mod-ollama-chat_config.cpp index 8bc1b1da3..3c503b100 100644 --- a/src/mod-ollama-chat_config.cpp +++ b/src/mod-ollama-chat_config.cpp @@ -42,6 +42,7 @@ uint32_t g_EventChatterMaxBotsPerPlayer = 2; // -------------------------------------------- std::string g_OllamaUrl = "http://localhost:11434/api/generate"; std::string g_OllamaModel = "llama3.2:1b"; +std::string g_OllamaApiKey = ""; uint32_t g_OllamaNumPredict = 40; float g_OllamaTemperature = 0.8f; float g_OllamaTopP = 0.95f; @@ -386,6 +387,7 @@ void LoadOllamaChatConfig() g_MaxBotsToPick = sConfigMgr->GetOption("OllamaChat.MaxBotsToPick", 2); g_OllamaUrl = sConfigMgr->GetOption("OllamaChat.Url", "http://localhost:11434/api/generate"); g_OllamaModel = sConfigMgr->GetOption("OllamaChat.Model", "llama3.2:1b"); + g_OllamaApiKey = sConfigMgr->GetOption("OllamaChat.ApiKey", ""); g_OllamaNumPredict = sConfigMgr->GetOption("OllamaChat.NumPredict", 40); g_OllamaTemperature = sConfigMgr->GetOption("OllamaChat.Temperature", 0.8f); g_OllamaTopP = sConfigMgr->GetOption("OllamaChat.TopP", 0.95f); diff --git a/src/mod-ollama-chat_config.h b/src/mod-ollama-chat_config.h index 37e66e26e..a09296070 100644 --- a/src/mod-ollama-chat_config.h +++ b/src/mod-ollama-chat_config.h @@ -43,6 +43,7 @@ extern uint32_t g_EventChatterMaxBotsPerPlayer; // -------------------------------------------- extern std::string g_OllamaUrl; extern std::string g_OllamaModel; +extern std::string g_OllamaApiKey; // optional Bearer extern uint32_t g_OllamaNumPredict; extern float g_OllamaTemperature; extern float g_OllamaTopP; diff --git a/src/mod-ollama-chat_httpclient.cpp b/src/mod-ollama-chat_httpclient.cpp index b2e3294ab..a1d48729c 100644 --- a/src/mod-ollama-chat_httpclient.cpp +++ b/src/mod-ollama-chat_httpclient.cpp @@ -19,7 +19,7 @@ OllamaHttpClient::~OllamaHttpClient() { } -std::string OllamaHttpClient::Post(const std::string& url, const std::string& jsonData) +std::string OllamaHttpClient::Post(const std::string& url, const std::string& jsonData, const std::string& bearerToken) { try { @@ -79,7 +79,9 @@ std::string OllamaHttpClient::Post(const std::string& url, const std::string& js {"User-Agent", "AzerothCore-OllamaChat/1.0"}, {"Accept", "application/json"} }; - + if (!bearerToken.empty()) + headers.emplace("Authorization", "Bearer " + bearerToken); + // Add ngrok bypass header if this is an ngrok URL if (host.find("ngrok") != std::string::npos || host.find("ngrok-free.app") != std::string::npos) { headers.emplace("ngrok-skip-browser-warning", "true"); @@ -112,7 +114,9 @@ std::string OllamaHttpClient::Post(const std::string& url, const std::string& js {"User-Agent", "AzerothCore-OllamaChat/1.0"}, {"Accept", "application/json"} }; - + if (!bearerToken.empty()) + headers.emplace("Authorization", "Bearer " + bearerToken); + // Add ngrok bypass header if this is an ngrok URL if (host.find("ngrok") != std::string::npos || host.find("ngrok-free.app") != std::string::npos) { headers.emplace("ngrok-skip-browser-warning", "true"); diff --git a/src/mod-ollama-chat_httpclient.h b/src/mod-ollama-chat_httpclient.h index ec51274e8..ec476cc2d 100644 --- a/src/mod-ollama-chat_httpclient.h +++ b/src/mod-ollama-chat_httpclient.h @@ -10,7 +10,7 @@ class OllamaHttpClient ~OllamaHttpClient(); // Make HTTP POST request to Ollama API - std::string Post(const std::string& url, const std::string& jsonData); + std::string Post(const std::string& url, const std::string& jsonData, const std::string& bearerToken = ""); // Set timeout for requests (in seconds) void SetTimeout(int seconds);