/* * Copyright (c) 2025 Aldrik Ramaekers * * 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 #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(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]; 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(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(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, };