summaryrefslogtreecommitdiff
path: root/src/providers/openAI.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/providers/openAI.cpp')
-rw-r--r--src/providers/openAI.cpp276
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