From ed6af7e08a4cec8de6ee8f5c8dd1e4389e4aca0d Mon Sep 17 00:00:00 2001 From: rfprod Date: Tue, 6 Oct 2026 22:31:05 +0400 Subject: [PATCH] feat(api): add model listing functionality and refine client requests - [x] implement `query_model_list` to fetch and display available local models; - [x] replace single `url` string with `OllamaServerURLs` struct for multiple endpoints; - [x] rename `curl_request` to `query_model` for better semantic clarity; - [x] move `init_curl` call from constructor to individual request methods; - [x] add HTTP status code validation and mandatory exit on non-200 responses; - [x] ensure CURL cleanup and header freeing occur immediately after each request; - [x] add error handling and process termination for JSON parsing failures; - [x] include prompt logging in the model query process; --- .github/workflows/trunk.yml | 8 +-- .github/workflows/validate-pr.yml | 8 +-- CMakeLists.txt | 2 +- src/main.cpp | 4 +- src/ollama_client.cpp | 99 +++++++++++++++++++++++++++---- src/ollama_client.hpp | 10 +++- tools/shell/format.sh | 8 ++- 7 files changed, 108 insertions(+), 31 deletions(-) diff --git a/.github/workflows/trunk.yml b/.github/workflows/trunk.yml index 80e90f9..5943389 100644 --- a/.github/workflows/trunk.yml +++ b/.github/workflows/trunk.yml @@ -105,13 +105,7 @@ jobs: - name: Check formatting if: ${{ steps.src-changed.outputs.change == 'true' || steps.deps-changed.outputs.change == 'true' }} - run: | - find src -name "*.cpp" -o -name "*.hpp" | xargs clang-format --dry-run -Werror - - - name: Lint sources (cppcheck) - if: ${{ steps.src-changed.outputs.change == 'true' || steps.deps-changed.outputs.change == 'true' }} - run: | - cppcheck --enable=all --error-exitcode=1 --suppress=missingIncludeSystem src/ + run: bash ./tools/shell/format.sh - name: Lint sources (clang-tidy) if: ${{ steps.src-changed.outputs.change == 'true' || steps.deps-changed.outputs.change == 'true' }} diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml index 28270d1..b473ed7 100644 --- a/.github/workflows/validate-pr.yml +++ b/.github/workflows/validate-pr.yml @@ -108,13 +108,7 @@ jobs: - name: Check formatting if: ${{ steps.src-changed.outputs.change == 'true' || steps.deps-changed.outputs.change == 'true' }} - run: | - find src -name "*.cpp" -o -name "*.hpp" | xargs clang-format --dry-run -Werror - - - name: Lint sources (cppcheck) - if: ${{ steps.src-changed.outputs.change == 'true' || steps.deps-changed.outputs.change == 'true' }} - run: | - cppcheck --enable=all --error-exitcode=1 --suppress=missingIncludeSystem src/ + run: bash ./tools/shell/format.sh - name: Lint sources (clang-tidy) if: ${{ steps.src-changed.outputs.change == 'true' || steps.deps-changed.outputs.change == 'true' }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 12b1c63..faadeac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.20) -project(cpp-workspace VERSION 0.1.0 LANGUAGES CXX) +project(cpp-workspace VERSION 0.1.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/src/main.cpp b/src/main.cpp index fb7a29c..aa62c42 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -30,12 +30,14 @@ int main(int argc, const char* argv[]) { OllamaClient client = OllamaClient(); + client.query_model_list(); + struct OllamaPromptConfig config; config.model = model; config.prompt = prompt; config.stream = stream; - client.curl_request(config); + client.query_model(config); return 0; } diff --git a/src/ollama_client.cpp b/src/ollama_client.cpp index 78c1bce..5e0a555 100644 --- a/src/ollama_client.cpp +++ b/src/ollama_client.cpp @@ -15,13 +15,15 @@ static size_t WriteCallback(char* contents, size_t size, size_t nmemb, std::stri // Create an instance of OllamaClient. OllamaClient::OllamaClient() { - this->url = "http://localhost:11434/api/generate"; + this->urls = { + generate_url : "http://localhost:11434/api/generate", + tags_url : "http://localhost:11434/api/tags" + }; this->headers = nullptr; + this->curl = nullptr; this->check_ollama(); - this->init_curl(); - printf("\n"); } @@ -70,6 +72,7 @@ void OllamaClient::process_response(std::string response) { << std::endl; } catch (const json::exception& err) { fprintf(stderr, "JSON parse error: %s\n", err.what()); + exit(1); } } @@ -94,6 +97,7 @@ void OllamaClient::process_response_stream(std::string response) { } } catch (const json::exception& err) { fprintf(stderr, "JSON parse error: %s\n", err.what()); + exit(1); } } } @@ -101,8 +105,12 @@ void OllamaClient::process_response_stream(std::string response) { std::cout << std::endl; } -// Send a request to the Ollama endpoint using CURL. -void OllamaClient::curl_request(OllamaPromptConfig config) { +// Send a request to the Ollama `generate` endpoint using CURL. +void OllamaClient::query_model(OllamaPromptConfig config) { + this->init_curl(); + + printf("💬 prompt: %s\n", config.prompt.c_str()); + json request_payload = { {"model", config.model}, {"prompt", config.prompt}, {"stream", config.stream}}; @@ -110,7 +118,7 @@ void OllamaClient::curl_request(OllamaPromptConfig config) { std::string response; - curl_easy_setopt(this->curl, CURLOPT_URL, this->url.c_str()); + curl_easy_setopt(this->curl, CURLOPT_URL, this->urls.generate_url.c_str()); curl_easy_setopt(this->curl, CURLOPT_POSTFIELDS, post_data.c_str()); curl_easy_setopt(this->curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(this->curl, CURLOPT_WRITEDATA, &response); @@ -121,12 +129,21 @@ void OllamaClient::curl_request(OllamaPromptConfig config) { this->headers = curl_slist_append(this->headers, "Content-Type: application/json"); curl_easy_setopt(this->curl, CURLOPT_HTTPHEADER, this->headers); - CURLcode res = curl_easy_perform(curl); + CURLcode result = curl_easy_perform(curl); + + long status = 0; + curl_easy_getinfo(this->curl, CURLINFO_RESPONSE_CODE, &status); - if (res != CURLE_OK) { - fprintf(stderr, "CURL error: %s\n", curl_easy_strerror(res)); - curl_slist_free_all(this->headers); - curl_easy_cleanup(this->curl); + curl_easy_cleanup(this->curl); + curl_slist_free_all(this->headers); + this->headers = nullptr; + + if (result != CURLE_OK) { + fprintf(stderr, "CURL error: %s\n", curl_easy_strerror(result)); + exit(1); + } + if (status != 200) { + fprintf(stderr, "Ollama returned HTTP status: %ld\n", status); exit(1); } @@ -135,7 +152,65 @@ void OllamaClient::curl_request(OllamaPromptConfig config) { } else { this->process_response(response); } +} + +// Send a request to the Ollama endpoint for listing local models using CURL. +void OllamaClient::query_model_list() { + this->init_curl(); + + std::string response; + + curl_easy_setopt(this->curl, CURLOPT_URL, this->urls.tags_url.c_str()); + curl_easy_setopt(this->curl, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(this->curl, CURLOPT_WRITEDATA, &response); + + curl_easy_setopt(this->curl, CURLOPT_XFERINFOFUNCTION, ProgressSpinner::progress_callback); + curl_easy_setopt(this->curl, CURLOPT_NOPROGRESS, 0L); + + this->headers = curl_slist_append(this->headers, "Content-Type: application/json"); + curl_easy_setopt(this->curl, CURLOPT_HTTPHEADER, this->headers); + + CURLcode result = curl_easy_perform(this->curl); + + long status = 0; + curl_easy_getinfo(this->curl, CURLINFO_RESPONSE_CODE, &status); - curl_slist_free_all(this->headers); curl_easy_cleanup(this->curl); + curl_slist_free_all(this->headers); + this->headers = nullptr; + + if (result != CURLE_OK) { + fprintf(stderr, "CURL error: %s\n", curl_easy_strerror(result)); + exit(1); + } + if (status != 200) { + fprintf(stderr, "Ollama returned HTTP status: %ld\n", status); + exit(1); + } + + try { + const json data = json::parse(response); + + if (!data.contains("models") || !data["models"].is_array()) { + fprintf(stderr, "Response has no models array.\n"); + exit(1); + } + + printf("📃 available models:\n"); + + for (const auto& model : data["models"]) { + const std::string name = model.value("name", "unknown"); + const std::string modified_at = model.value("modified_at", "unknown"); + const std::string size = + model.contains("size") + ? std::to_string(model["size"].get() / 1024 / 1024) + " Mb" + : "size unknown"; + + std::cout << modified_at << "\t (" << size << ") \t" << name << "\n"; + } + printf("\n"); + } catch (const json::exception& err) { + fprintf(stderr, "JSON parse error: %s\n", err.what()); + exit(1); + } } diff --git a/src/ollama_client.hpp b/src/ollama_client.hpp index 17276c5..de02c45 100644 --- a/src/ollama_client.hpp +++ b/src/ollama_client.hpp @@ -8,10 +8,15 @@ struct OllamaPromptConfig { bool stream; }; +struct OllamaServerURLs { + std::string generate_url; + std::string tags_url; +}; + class OllamaClient { private: CURL* curl; - std::string url; + OllamaServerURLs urls; struct curl_slist* headers; ProgressSpinner spinner{ProgressSpinner()}; @@ -26,5 +31,6 @@ class OllamaClient { public: OllamaClient(); - void curl_request(OllamaPromptConfig config); + void query_model(OllamaPromptConfig config); + void query_model_list(); }; diff --git a/tools/shell/format.sh b/tools/shell/format.sh index c210f23..c67d0a6 100644 --- a/tools/shell/format.sh +++ b/tools/shell/format.sh @@ -4,4 +4,10 @@ set -euo pipefail clang-format src/* -i -Werror -cppcheck --enable=all --error-exitcode=1 --suppress=missingIncludeSystem src/ +cppcheck --std=c++20 \ + --enable=all \ + --error-exitcode=1 \ + --suppress=missingIncludeSystem \ + --suppress=unusedLabel \ + -I src/ \ + src/