/* * 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 #include #include #define CPPHTTPLIB_OPENSSL_SUPPORT #include "httplib.h" #include "log.hpp" #include "ai_service.hpp" static char *extract_json_value(const char *json, const char *key, char *out, size_t out_size, int skip = 0) { char pattern[128]; snprintf(pattern, sizeof(pattern), "\"%s\"", key); const char *pos = strstr(json, pattern); while(skip > 0) { pos = strstr(pos+1, pattern); skip--; } if (!pos) return NULL; pos = strchr(pos, ':'); if (!pos) return NULL; pos++; // Skip whitespace and quotes while (*pos == ' ' || *pos == '\"') pos++; size_t i = 0; while (*pos && !(*pos == '\"' && *(pos-1) != '\\') && i < out_size - 1) { out[i++] = *pos++; } out[i] = '\0'; return out; } static const char* get_filename(const char* path) { const char* filename = strrchr(path, '/'); // for Unix-style paths if (filename) return filename + 1; // skip the '/' filename = strrchr(path, '\\'); // for Windows-style paths if (filename) return filename + 1; return path; // no slashes found, path itself is filename } static char *escape_quotes(const char *input, size_t buffer_size) { if (!input) return NULL; char *result = (char*)malloc(buffer_size + 100); // Ballpark if (!result) return NULL; const char *src = input; char *dst = result; while (*src) { if (*src == '"') { *dst++ = '\\'; *dst++ = '"'; } else if (*src == '\n') { // empty } else { *dst++ = *src; } src++; } *dst = '\0'; return result; } static char *unescape_quotes(char *input) { if (!input) return NULL; char *src = input; char *dst = input; while (*src) { if (*src == '\\' && *(src+1) == '"') { src++; } else if (*src == '\\' && *(src+1) == 'n') { src++;src++; } *dst++ = *src++; } *dst = '\0'; return input; } static bool _openAI_query_with_file(char* query, size_t query_length, char* file_id, char** response) { #define TESTING_IMPORT #ifndef TESTING_IMPORT 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 = escape_quotes(query, query_length); free(query); size_t body_size = query_length + 200; char* body = (char*)malloc(body_size); snprintf(body, body_size, "{\"model\":\"gpt-5-nano\", \"input\": [ { \"role\": \"user\", \"content\": [ { \"type\": \"input_file\", \"file_id\": \"%s\" }, " "{ \"type\": \"input_text\", \"text\": \"%s\" } ] } ] }", 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"); free(body); if (!res || res->status != 200) { log_error("ERROR Failed to query API."); return 0; } char* response_body = (char*)res->body.c_str(); #else char* response_body = "{\n \"id\": \"resp_68d9482030fc8196930b43b6b28feeb104e98afee829eee0\",\n \"object\": \"response\",\n \"created_at\": 1759070240,\n \"status\": \"completed\",\n \"background\": false,\n \"billing\": {\n \"payer\": \"developer\"\n },\n \"error\": null,\n \"incomplete_details\": null,\n \"instructions\": null,\n \"max_output_tokens\": null,\n \"max_tool_calls\": null,\n \"model\": \"gpt-5-2025-08-07\",\n \"output\": [\n {\n \"id\": \"rs_68d94821d1f0819694533a6ed7ed6b2904e98afee829eee0\",\n \"type\": \"reasoning\",\n \"summary\": []\n },\n {\n \"id\": \"msg_68d948a09e0c819696782e09c6b7626104e98afee829eee0\",\n \"type\": \"message\",\n \"status\": \"completed\",\n \"content\": [\n {\n \"type\": \"output_text\",\n \"annotations\": [],\n \"logprobs\": [],\n \"text\": \"\\n urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0\\n urn:fdc:peppol.eu:2017:poacc:billing:01:1.0\\n 586928\\n 2025-03-17\\n 2025-03-24\\n 380\\n EUR\\n \\n 699607\\n \\n \\n AR385893\\n Jouw bestelling : 420675-WWW.SCHROEVEN-EXPRESS.NL-14.03.25-18:31\\n \\n \\n 420675-WWW.SCHROEVEN-EXPRESS.NL-14.03.25-18:31\\n \\n \\n \\n \\n \\n \\n \\n \\n \\n R.C le mans B 302 494 224\\n \\n \\n Visserie-service\\n \\n \\n Z.A Nord\\n \\n Parce sur Sarthe\\n 72300\\n \\n \\n FR\\n \\n \\n \\n FR57 302 494 224\\n \\n VAT\\n \\n \\n \\n Visserie Service SAS\\n \\n \\n AMELIE L\\n 02.43.62.09.08\\n klantenservice@schroeven-express.nl\\n \\n \\n \\n \\n \\n \\n \\n cl585187\\n \\n \\n ALDRIK RAMAEKERS\\n \\n \\n KEERDERSTRAAT 81\\n \\n MAASTRICHT\\n 6226X\\n \\n \\n NL\\n \\n \\n \\n \\n \\n VAT\\n \\n \\n \\n ALDRIK RAMAEKERS\\n \\n \\n A RAMAEKERS\\n 31618260377\\n aldrikboy@gmail.com\\n \\n \\n \\n \\n 2025-03-17\\n \\n \\n KEERDERSTRAAT 81\\n \\n MAASTRICHT\\n 6226X\\n \\n \\n NL\\n \\n \\n \\n \\n \\n ALDRIK RAMAEKERS\\n \\n \\n \\n \\n \\n 586928\\n \\n FR76 1790 6001 1272 5017 0700 137\\n Visserie Service SAS\\n \\n \\n AGRIFRPP879\\n \\n \\n \\n \\n \\n \\n \\n \\n 2.59\\n \\n 12.36\\n 2.59\\n \\n \\n 21\\n \\n VAT\\n \\n \\n \\n \\n \\n 6.95\\n 12.36\\n 14.95\\n 14.95\\n \\n \\n 1\\n 500\\n 6.95\\n \\n false\\n Discount\\n \\n \\n \\n \\n \\n Metalen schroeven RVS A2 gefreesde kop Pozi N\\u00b01 M2X4 DIN 965 ISO 7046, VS0109, VS0110\\n \\n Internal Tax Rate ID\\n \\n \\n \\n \\n 21\\n \\n VAT\\n \\n \\n \\n \\n 1.39\\n \\n \\n\"\n }\n ],\n \"role\": \"assistant\"\n }\n ],\n \"parallel_tool_calls\": true,\n"; #endif *response = (char*)malloc(100000); memset(*response, 0, 100000); strncpy(*response, response_body, 100000); extract_json_value(*response, "text", *response, 100000); *response = unescape_quotes(*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 = get_filename(file_path); FILE* orig_file = fopen(file_path, "rb"); if (orig_file == NULL) { log_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]; snprintf(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) { log_error("ERROR Failed to create upload."); fclose(orig_file); return 0; } char upload_id[128]; extract_json_value(res->body.c_str(), "id", upload_id, sizeof(upload_id)); size_t part_size = 64000000; // 64mb log_info("Created upload %s with part size %zu.", upload_id, part_size); char *buffer = (char*)malloc(part_size); char completion_body[1048]; snprintf(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]; snprintf(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) { log_error("Failed to upload part %d.", part_number); free(buffer); fclose(orig_file); return 0; } else { char part_id[128]; extract_json_value(part_res->body.c_str(), "id", part_id, sizeof(part_id)); if (part_number == 0) snprintf(completion_body+strlen(completion_body), sizeof(completion_body)-strlen(completion_body), "\"%s\"", part_id); if (part_number != 0) snprintf(completion_body+strlen(completion_body), sizeof(completion_body)-strlen(completion_body), ", \"%s\"", part_id); } log_info("Uploaded part %d\n", part_number); part_number++; } snprintf(completion_body+strlen(completion_body), sizeof(completion_body)-strlen(completion_body), "]}"); free(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) { log_error("ERROR Failed to complete upload."); return 0; } char* completion_body_response = (char*)complete_res->body.c_str(); extract_json_value(completion_body_response, "id", file_id, file_id_len, 1); return 1; } ai_provider_impl _chatgpt_api_provider = { _openAI_upload_file, _openAI_query_with_file, };