diff options
Diffstat (limited to 'src/providers/openAI.cpp')
| -rw-r--r-- | src/providers/openAI.cpp | 276 |
1 files changed, 276 insertions, 0 deletions
diff --git a/src/providers/openAI.cpp b/src/providers/openAI.cpp new file mode 100644 index 0000000..d1495dc --- /dev/null +++ b/src/providers/openAI.cpp @@ -0,0 +1,276 @@ +/* +* Copyright (c) 2025 Aldrik Ramaekers <aldrik.ramaekers@gmail.com> +* +* Permission to use, copy, modify, and/or distribute this software for any +* purpose with or without fee is hereby granted, provided that the above +* copyright notice and this permission notice appear in all copies. +* +* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#include <threads.h> + +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include "httplib.h" + +#include "memops.hpp" +#include "strops.hpp" +#include "logger.hpp" +#include "importer.hpp" + +static bool _openAI_batch_query_with_file(const char** queries, size_t query_count, char* file_id, invoice* buffer, importer::batch_query_response_handler response_handler) +{ + const char *api_key = administration::get_ai_service().api_key_public; + httplib::SSLClient cli("api.openai.com", 443); + + thrd_t threads[20]; + assert(query_count <= 20); + + for (u32 i = 0; i < query_count; i++) + { + auto* func = new auto([&api_key, &cli, i, &file_id, &response_handler, &buffer, &queries]() { + char* query_escaped = strops::prep_str_for_json(queries[i], 1000); + + size_t body_size = 1000; // Ballpark + char* body = (char*)memops::alloc(body_size); + strops::format(body, body_size, + "{ \"model\":\"%s\", " + " \"input\": [ " + " { \"role\": \"user\", " + " \"content\": [ " + " { \"type\": \"input_file\", \"file_id\": \"%s\" }, " + " { \"type\": \"input_text\", \"text\": \"%s\" } " + " ] " + " }" + "], " + " \"text\": { \"format\": { \"type\": \"json_object\" } } " + "}", administration::get_ai_service().model_name, file_id, query_escaped); + + httplib::Headers headers; + headers.insert(std::make_pair("Authorization", std::string("Bearer ") + api_key)); + + httplib::Result res = cli.Post("/v1/responses", headers, body, "application/json"); + memops::unalloc(body); + + if (!res || res->status != 200) { + logger::error("ERROR Failed to query API."); + logger::error(res->body.c_str()); + return 0; + } + + char* response_body = (char*)res->body.c_str(); + char* response = (char*)memops::alloc(5000); + memops::zero(response, 5000); + strops::copy(response, response_body, 5000); + + strops::get_json_value(response, "text", response, 5000); + strops::unprep_str_from_json(response); + + response_handler(buffer, response); + + memops::unalloc(response); + memops::unalloc(query_escaped); + return 1; + }); + + auto trampoline = [](void* arg) -> int { + auto* f = static_cast<decltype(func)>(arg); + (*f)(); + delete f; + return 0; + }; + + thrd_create(&threads[i], trampoline, func); + } + + for (u32 i = 0; i < query_count; i++) thrd_join(threads[i], nullptr); + + return 1; +} + +static bool _openAI_query_with_file(const char* query, size_t query_length, char* file_id, char** response) +{ + const char *api_key = administration::get_ai_service().api_key_public; + + httplib::SSLClient cli("api.openai.com", 443); + //cli.enable_server_certificate_verification(false); + + char* query_escaped = strops::prep_str_for_json(query, query_length); + //memops::unalloc(query); + + size_t body_size = query_length + 200; + char* body = (char*)memops::alloc(body_size); + strops::format(body, body_size, + "{\"model\":\"%s\", \"input\": [ { \"role\": \"user\", \"content\": [ { \"type\": \"input_file\", \"file_id\": \"%s\" }, " + "{ \"type\": \"input_text\", \"text\": \"%s\" } ] } ] }", administration::get_ai_service().model_name, file_id, query_escaped); + + httplib::Headers headers; + headers.insert(std::make_pair("Authorization", std::string("Bearer ") + api_key)); + + httplib::Result res = cli.Post("/v1/responses", headers, body, "application/json"); + memops::unalloc(body); + + if (!res || res->status != 200) { + logger::error("ERROR Failed to query API."); + logger::error(res->body.c_str()); + return 0; + } + + char* response_body = (char*)res->body.c_str(); + *response = (char*)memops::alloc(100000); + memops::zero(*response, 100000); + strops::copy(*response, response_body, 100000); + + strops::get_json_value(*response, "text", *response, 100000); + *response = strops::unprep_str_from_json(*response); + + return 1; +} + +static bool _openAI_upload_file(char* file_path, char* file_id, size_t file_id_len) +{ + const char *api_key = administration::get_ai_service().api_key_public; + const char *filename = strops::get_filename(file_path); + + FILE* orig_file = fopen(file_path, "rb"); + if (orig_file == NULL) { + logger::error("ERROR: file to upload could not be opened."); + return 0; + } + + fseek(orig_file, 0L, SEEK_END); + long sz = ftell(orig_file); + fseek(orig_file, 0, SEEK_SET); + + httplib::SSLClient cli("api.openai.com", 443); + //cli.enable_server_certificate_verification(false); + + char body[512]; + strops::format(body, sizeof(body), "{\"filename\":\"%s\",\"purpose\":\"user_data\", \"bytes\": %d, \"mime_type\": \"application/pdf\", \"expires_after\": { \"anchor\": \"created_at\", \"seconds\": 3600 } }", filename, sz); + + httplib::Headers headers; + headers.insert(std::make_pair("Authorization", std::string("Bearer ") + api_key)); + + httplib::Result res = cli.Post("/v1/uploads", headers, body, "application/json"); + if (!res || res->status != 200) { + logger::error("ERROR Failed to create upload."); + logger::error(res->body.c_str()); + fclose(orig_file); + return 0; + } + + char upload_id[128]; + strops::get_json_value(res->body.c_str(), "id", upload_id, sizeof(upload_id)); + size_t part_size = 64000000; // 64mb + logger::info("Created upload %s with part size %zu.", upload_id, part_size); + + char *buffer = (char*)memops::alloc(part_size); + + char completion_body[1048]; + strops::format(completion_body, sizeof(completion_body), "{\"part_ids\": ["); + + int part_number = 0; + while (1) { + size_t read_bytes = fread(buffer, 1, part_size, orig_file); + if (read_bytes == 0) break; + + httplib::Headers part_headers; + part_headers.insert(std::make_pair("Authorization", std::string("Bearer ") + api_key)); + + std::string chunk(buffer, read_bytes); + + httplib::UploadFormDataItems items = { + {"data", chunk, filename, "application/octet-stream"} + }; + + char path[256]; + strops::format(path, sizeof(path), "/v1/uploads/%s/parts?part_number=%d", upload_id, part_number); + + httplib::Result part_res = cli.Post(path, part_headers, items); + + if (!part_res || part_res->status != 200) { + logger::error("Failed to upload part %d.", part_number); + logger::error(part_res->body.c_str()); + memops::unalloc(buffer); + fclose(orig_file); + return 0; + } + else { + char part_id[128]; + strops::get_json_value(part_res->body.c_str(), "id", part_id, sizeof(part_id)); + if (part_number == 0) strops::format(completion_body+strops::length(completion_body), sizeof(completion_body)-strops::length(completion_body), "\"%s\"", part_id); + if (part_number != 0) strops::format(completion_body+strops::length(completion_body), sizeof(completion_body)-strops::length(completion_body), ", \"%s\"", part_id); + } + + logger::info("Uploaded part %d\n", part_number); + part_number++; + } + + strops::format(completion_body+strops::length(completion_body), sizeof(completion_body)-strops::length(completion_body), "]}"); + + memops::unalloc(buffer); + fclose(orig_file); + + // ---------- Step 3: Complete upload ---------- + + httplib::Result complete_res = cli.Post((std::string("/v1/uploads/") + upload_id + "/complete").c_str(), + headers, completion_body, "application/json"); + + + if (!complete_res || complete_res->status != 200) { + logger::error("ERROR Failed to complete upload."); + logger::error(complete_res->body.c_str()); + return 0; + } + + char* completion_body_response = (char*)complete_res->body.c_str(); + strops::get_json_value(completion_body_response, "id", file_id, file_id_len, 1); + + return 1; +} + +static bool _openAI_get_available_models(importer::model_list_request* buffer) +{ + const char *api_key = administration::get_ai_service().api_key_public; + + httplib::SSLClient cli("api.openai.com", 443); + + httplib::Headers headers; + headers.insert(std::make_pair("Authorization", std::string("Bearer ") + api_key)); + + httplib::Result res = cli.Get("/v1/models", headers); + if (!res || res->status != 200) { + logger::error("ERROR Failed to get models list."); + logger::error(res->body.c_str()); + return 0; + } + + char* completion_body_response = (char*)res->body.c_str(); + + u32 count = 0; + char model_name[MAX_LEN_SHORT_DESC]; + + while(1) { + if (!strops::get_json_value(completion_body_response, "id", model_name, MAX_LEN_SHORT_DESC, count++)) break; + if (count == MAX_MODEL_LIST_RESULT_COUNT) break; + + strops::copy(buffer->result[buffer->result_count++], model_name, MAX_LEN_SHORT_DESC); + } + + return 1; +} + +importer::ai_provider_impl _chatgpt_api_provider = { + "OpenAI", + "gpt-5-nano", + _openAI_upload_file, + _openAI_query_with_file, + _openAI_batch_query_with_file, + _openAI_get_available_models, +};
\ No newline at end of file |
