37 return "Unknown result";
63 configured(false), connected(false), lastError(
LLM_OK),
65 globalConnection(false), owner(NULL), ownerEndCallback(NULL),
66 ownerNotified(false), cancelRequested(false), connectTimeMS(0),
72 currentStats.uptimeMS =
GetTimeNow() - connectTimeMS;
73 currentStats.ended =
true;
81 ownerEndCallback = callback;
82 ownerNotified =
false;
85void LLMConnection::notifyOwnerEnded() {
86 if (ownerEndCallback && !ownerNotified) {
88 ownerEndCallback(owner,
this, currentStats);
98void LLMConnection::clearConfig() {
104 vendorTypeName.clear();
107 globalConnection =
false;
108 schemaVersion.clear();
111 apiKeySecret.clear();
112 requestTemplate.clear();
113 responseTemplate.clear();
115 constraints = LLMConstraints();
117 selectedModel.clear();
118 currentStats = LLMStats();
119 cancelRequested =
false;
130bool LLMConnection::nodeHasInlineSecret(
const XMLNode& node) {
131 if (node.isAttributeSet(
"apikey"))
return true;
132 if (node.nChildNode(
"apikey") > 0)
return true;
136void LLMConnection::resolveSecret() {
137 apiKeySecret.clear();
138 if (apiKeyRef.empty())
return;
145 if (!
strnicmp(apiKeyRef.c_str(),
"env:", 4)) {
146 const char* env = getenv(apiKeyRef.c_str() + 4);
147 if (env && *env) apiKeySecret = env;
150 if (!
strnicmp(apiKeyRef.c_str(),
"file:", 5)) {
151 FILE* f = fopen(apiKeyRef.c_str() + 5,
"rb");
156 while ((n = fread(buf, 1,
sizeof(buf), f)) > 0) content.append(buf, n);
158 while (!content.empty() && (content[content.size()-1] ==
'\n' ||
159 content[content.size()-1] ==
'\r' || content[content.size()-1] ==
' ' ||
160 content[content.size()-1] ==
'\t'))
161 content.erase(content.size()-1);
162 apiKeySecret = content;
165 const char* env = getenv(apiKeyRef.c_str());
166 if (env && *env) { apiKeySecret = env;
return; }
167 if (secretsFile.empty())
return;
168 FILE* f = fopen(secretsFile.c_str(),
"r");
171 while (fgets(line,
sizeof(line), f)) {
172 char* eq = strchr(line,
'=');
177 size_t len = strlen(val);
178 while (len && (val[len-1] ==
'\n' || val[len-1] ==
'\r' || val[len-1] ==
' ' || val[len-1] ==
'\t'))
180 if (!
stricmp(line, apiKeyRef.c_str()) && len) {
191 XMLNode node = XMLNode::parseString(xml,
"llm", &results);
200 const char* nodeName = llmNode.
getName();
206 globalConnection =
attrTrue(llmNode,
"global");
207 if ((v = llmNode.
getAttribute(
"schemaversion")) != NULL) schemaVersion = v;
208 if ((v = llmNode.
getAttribute(
"apikeyref")) != NULL) apiKeyRef = v;
209 if ((v = llmNode.
getAttribute(
"secretsfile")) != NULL) secretsFile = v;
210 if ((v = llmNode.
getAttribute(
"maxtokensperrequest")) != NULL) constraints.maxTokensPerRequest = atoi(v);
211 if ((v = llmNode.
getAttribute(
"maxrequestspermin")) != NULL) constraints.maxRequestsPerMin = atoi(v);
212 if ((v = llmNode.
getAttribute(
"maxconcurrent")) != NULL) constraints.maxConcurrent = atoi(v);
213 if ((v = llmNode.
getAttribute(
"costceiling")) != NULL) constraints.costCeiling = atof(v);
216 if (res !=
LLM_OK)
return fail(res);
239 if (tres !=
LLM_OK)
return tres;
241 for (
int i = 0; i < n; i++) {
248 if (hv) header.
value = hv;
249 headers.push_back(header);
252 if (!t.isEmpty() && t.getText()) requestTemplate = t.getText();
254 if (!t.isEmpty() && t.getText()) responseTemplate = t.getText();
256 if (!modelsNode.isEmpty()) {
257 n = modelsNode.nChildNode(
"model");
258 for (
int i = 0; i < n; i++) {
259 const char* mn = modelsNode.getChildNode(
"model", i).getAttribute(
"name");
261 models.push_back(mn);
263 const char* def = modelsNode.getAttribute(
"default");
266 for (
size_t i = 0; i < models.size(); i++)
267 if (!
stricmp(models[i].c_str(), def)) { selectedModel = models[i]; found =
true;
break; }
270 else if (!models.empty()) selectedModel = models[0];
281 if (modelName && *modelName) {
282 for (
size_t i = 0; i < models.size(); i++) {
283 if (!
stricmp(models[i].c_str(), modelName)) {
284 selectedModel = models[i];
298 for (
const char* p = s; *p; p++) {
299 unsigned char c = (
unsigned char)*p;
301 case '"': out +=
"\\\"";
break;
302 case '\\': out +=
"\\\\";
break;
303 case '\b': out +=
"\\b";
break;
304 case '\f': out +=
"\\f";
break;
305 case '\n': out +=
"\\n";
break;
306 case '\r': out +=
"\\r";
break;
307 case '\t': out +=
"\\t";
break;
311 snprintf(buf,
sizeof(buf),
"\\u%04x", c);
320static void llmReplaceAll(std::string& text,
const char* what,
const std::string& with) {
321 size_t wlen = strlen(what), pos = 0;
322 while ((pos = text.find(what, pos)) != std::string::npos) {
323 text.replace(pos, wlen, with);
324 pos += with.length();
329std::string LLMConnection::applyTemplates(
const std::string& text,
const char* input)
const {
330 std::string out = text;
341 while (i < count && t[i].start >= 0 && t[i].start < end) i++;
348bool LLMConnection::extractJSONPath(
const char* json, uint32 size,
const char* path,
350 if (!json || !size || !path || !*path)
return false;
353 int count =
jsmn_parse(&parser, json, size, NULL, 0);
354 if (count <= 0)
return false;
355 std::vector<jsmntok_t> tokens((
size_t)count);
357 if (
jsmn_parse(&parser, json, size, &tokens[0], (
unsigned int)count) != count)
361 const char* p = path;
363 if (*p ==
'.') { p++;
continue; }
365 int idx = (int)strtol(p + 1, (
char**)&p, 10);
366 if (*p !=
']')
return false;
368 if (t[cur].type !=
JSMN_ARRAY || idx < 0 || idx >= t[cur].size)
return false;
370 for (
int n = 0; n < idx; n++) child =
llmSkipToken(t, child, count);
374 const char* segStart = p;
375 while (*p && *p !=
'.' && *p !=
'[') p++;
376 std::string key(segStart, (
size_t)(p - segStart));
380 for (
int n = 0; n < t[cur].
size && child < count; n++) {
381 int keyLen = t[child].
end - t[child].
start;
382 if (t[child].type ==
JSMN_STRING && (
int)key.length() == keyLen &&
383 strncmp(json + t[child].start, key.c_str(), (
size_t)keyLen) == 0) {
390 if (!found)
return false;
392 out.assign(json + t[cur].start, (
size_t)(t[cur].end - t[cur].start));
398 if (!apiKeyRef.empty() && apiKeySecret.empty()) {
406 ownerNotified =
false;
413 cancelRequested =
true;
415 currentStats.uptimeMS =
GetTimeNow() - connectTimeMS;
416 currentStats.ended =
true;
430 out.reserve(in.length());
431 for (
size_t i = 0; i < in.length(); i++) {
432 if (in[i] ==
'\\' && i + 1 < in.length()) {
435 case 'n': out +=
'\n';
break;
436 case 't': out +=
'\t';
break;
437 case 'r': out +=
'\r';
break;
438 case 'b': out +=
'\b';
break;
439 case 'f': out +=
'\f';
break;
441 if (i + 4 < in.length()) {
442 unsigned int cp = (
unsigned int)strtoul(in.substr(i + 1, 4).c_str(), NULL, 16);
444 if (cp < 0x80) out += (char)cp;
445 else if (cp < 0x800) {
446 out += (char)(0xC0 | (cp >> 6));
447 out += (char)(0x80 | (cp & 0x3F));
450 out += (char)(0xE0 | (cp >> 12));
451 out += (char)(0x80 | ((cp >> 6) & 0x3F));
452 out += (char)(0x80 | (cp & 0x3F));
456 default: out += c;
break;
467void LLMConnection::accountUsageJSON(
const char* json, uint32 size) {
468 if (!json || !size)
return;
470 if (extractJSONPath(json, size,
"usage.prompt_tokens", tok) ||
471 extractJSONPath(json, size,
"usage.input_tokens", tok) ||
472 extractJSONPath(json, size,
"usage.inputTokens", tok))
473 currentStats.
tokensIn += (uint64)strtoull(tok.c_str(), NULL, 10);
474 if (extractJSONPath(json, size,
"usage.completion_tokens", tok) ||
475 extractJSONPath(json, size,
"usage.output_tokens", tok) ||
476 extractJSONPath(json, size,
"usage.outputTokens", tok))
477 currentStats.
tokensOut += (uint64)strtoull(tok.c_str(), NULL, 10);
488LLMResult LLMConnection::resolveStreamTransport(
const char* transportAttr) {
489 if (transportAttr && *transportAttr) {
506class LLMPrefixByteSource :
public cmsdk::StreamByteSource {
508 LLMPrefixByteSource(
const std::string& prefixBytes,
509 cmsdk::StreamByteSource& nextSource,
const bool& cancelledFlag,
510 const volatile bool& resetFlag, uint64& bodyByteCounter)
511 : prefix(prefixBytes), pos(0), next(nextSource), cancelled(cancelledFlag),
512 resetRequested(resetFlag), bodyBytes(bodyByteCounter) {}
515 if (pos < prefix.size()) {
516 got = prefix.size() - pos;
517 if (got > maxSize) got = maxSize;
518 memcpy(buf, prefix.data() + pos, got);
524 bodyBytes += (uint64)got;
530 cmsdk::StreamByteSource& next;
531 const bool& cancelled;
532 const volatile bool& resetRequested;
541LLMResult LLMConnection::pumpStreamReply(
void* conPtr,
543 NetworkConnection* con = (NetworkConnection*)conPtr;
544 cmsdk::ConnectionByteSource<NetworkConnection> raw(*con, 250);
548 size_t headerEnd = std::string::npos;
550 for (
size_t iter = 0; iter < 1000 && headerEnd == std::string::npos; iter++) {
557 head.append(buf, got);
559 headerEnd = head.find(
"\r\n\r\n");
563 currentStats.bytesReceived += (uint64)head.size();
566 size_t sp = head.find(
' ');
567 int status = (sp != std::string::npos) ? atoi(head.c_str() + sp + 1) : 0;
571 std::string leftover = head.substr(headerEnd + 4);
572 bool cancelled =
false;
573 LLMPrefixByteSource src(leftover, raw, cancelled, cancelRequested,
574 currentStats.bytesReceived);
578 auto deliver = [&](
const char* text,
size_t size) {
579 if (cancelled || cancelRequested || !size)
return;
580 currentStats.replyChunks++;
581 if (fullReply) fullReply->append(text, size);
582 if (!callback(text, (uint32)size, userData)) cancelled =
true;
588 auto deliverJSON = [&](
const std::string& json) {
589 accountUsageJSON(json.data(), (uint32)json.size());
590 std::string extracted;
591 if (extractJSONPath(json.data(), (uint32)json.size(),
592 responseTemplate.c_str(), extracted)) {
594 deliver(text.data(), text.size());
600 switch (streamTransport) {
602 cmsdk::SSEEventParser parser;
604 [&](
const std::string& ev) { deliverJSON(ev); });
608 cmsdk::EventStreamFrameParser parser;
610 [&](
const cmsdk::EventStreamFrameParser::Frame& f) {
616 cmsdk::HTTPChunkedDecoder dec;
618 [&](
const char* data,
size_t size) { deliver(data, size); });
641 if (fullReply) fullReply->clear();
648 cancelRequested =
false;
654 bool useSSL = (
stricmp(protocol.c_str(),
"https") == 0);
655 if (!useSSL &&
stricmp(protocol.c_str(),
"http"))
658 if (!port) port = useSSL ? 443 : 80;
660 if (!uri.length()) uri =
"/";
663 std::string body = applyTemplates(requestTemplate, input);
664 std::map<std::string, std::string> headerEntries;
665 for (
size_t i = 0; i < headers.size(); i++)
666 headerEntries[headers[i].name] = applyTemplates(headers[i].value, NULL);
668 currentStats.requestCount++;
669 currentStats.bytesSent += (uint64)body.length();
678 ? sslCon.
connect(host.c_str(), port, location, 30000)
679 : tcpCon.
connect(host.c_str(), port, location, 30000);
684 body.c_str(),
"application/json", (uint32)body.length(),
false, 0);
690 LLMResult res = pumpStreamReply(con, callback, userData, fullReply);
703 std::string body = applyTemplates(requestTemplate, input);
704 std::map<std::string, std::string> headerEntries;
705 for (
size_t i = 0; i < headers.size(); i++)
706 headerEntries[headers[i].name] = applyTemplates(headers[i].value, NULL);
708 currentStats.requestCount++;
709 currentStats.bytesSent += (uint64)body.length();
712 headerEntries, body.c_str(),
"application/json", (uint32)body.length());
715 uint32 contentSize = 0;
716 const char* content = httpReply->
getContent(contentSize);
717 currentStats.bytesReceived += contentSize;
718 uint8 status = httpReply->
type;
725#ifdef PSY_LLM_DEBUG_HTTP
726 printf(
"LLM DEBUG non-200: status=%u content(%u)='%.300s'\n",
727 (
unsigned)status, contentSize, content ? content :
"");
732 if (!content || !contentSize) {
738 accountUsageJSON(content, contentSize);
740 std::string extracted;
741 if (!extractJSONPath(content, contentSize, responseTemplate.c_str(), extracted)) {
753 if (connected && connectTimeMS)
HTML/URL helper utilities: entity encoding/decoding, MIME type lookup and URL component parsing.
Connection/channel management layer: multi-protocol listeners, typed dispatch, HTTP client — and the ...
#define HTTP_MALFORMED_URL
400 Bad Request
#define HTTP_SERVER_UNAVAILABLE
500 (backend server unavailable)
static bool SendHTTPRequest(NetworkConnection *con, HTTPRequest *req)
Serialise and send a request.
A parsed or generated HTTP response.
const char * getContent(uint32 &size)
Get the (decoded) response body.
uint8 type
HTTP_* status id of this reply.
A parsed or generated HTTP request (also used for WebSocket upgrade handshakes).
bool createRequest(uint8 type, const char *host, const char *uri, const char *content, uint32 contentSize, bool keepAlive, uint64 ifModifiedSince)
Build a simple request with optional raw body.
LLMResult interact(const char *input, std::string &reply)
LLMResult configure(const XMLNode &llmNode)
void setOwner(void *owner, LLMOwnerEndCallback callback)
LLMResult selectModel(const char *modelName)
LLMResult configureFromString(const char *xml)
LLMResult interactStream(const char *input, LLMStreamCallback callback, void *userData, std::string *fullReply=NULL)
std::vector< std::string > listModels() const
Abstract base class for all point-to-point network connections.
virtual bool disconnect(uint16 error=0)
Close the connection and release the socket.
Central owner of all channels, listeners and connections in a process.
SSL/TLS-encrypted TCP connection (OpenSSL) with configurable peer verification.
bool connect(SOCKET s, uint64 localAddr, NetworkDataReceiver *receiver=NULL)
Adopt an already-accepted socket and perform the server-side TLS handshake.
Plain TCP stream connection (client-initiated or accepted from a listener).
bool connect(SOCKET s, uint64 localAddr, NetworkDataReceiver *receiver=NULL)
Adopt an already-connected socket (server side, from a TCPListener).
uint64 GetTimeNow()
Return the current absolute time (µs since year 0) according to the TMC.
Third-party (vendored): jsmn minimalistic JSON tokenizer by Serge Zaitsev (MIT licence),...
int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, jsmntok_t *tokens, unsigned int num_tokens)
Run JSON parser.
void jsmn_init(jsmn_parser *parser)
Create JSON parser over an array of tokens.
std::string GetURIFromURL(std::string url)
Extract the URI (path plus query) from a URL.
std::string GetProtocolFromURL(std::string url)
Extract the protocol/scheme from a URL.
std::string GetHostFromURL(std::string url)
Extract the host name (or IP literal) from a URL.
uint16 GetPortFromURL(std::string url)
Extract the port number from a URL.
static void llmReplaceAll(std::string &text, const char *what, const std::string &with)
static std::string llmJSONUnescape(const std::string &in)
static std::string llmJSONEscape(const char *s)
const char * LLMResultText(LLMResult result)
struct XMLDLLENTRY cmlabs::XMLNode XMLNode
const char * LLMStreamTransportText(LLMStreamTransport transport)
const char * LLMVendorTypeText(LLMVendorType type)
static int llmSkipToken(const jsmntok_t *t, int i, int count)
bool(* LLMStreamCallback)(const char *token, uint32 size, void *userData)
void(* LLMOwnerEndCallback)(void *owner, LLMConnection *connection, const LLMStats &finalStats)
static bool attrTrue(const XMLNode &node, const char *attr)
@ LLM_ERR_CONFIG_NO_VENDOR
@ LLM_ERR_CONFIG_NO_ENDPOINT
@ LLM_ERR_SECRET_UNRESOLVED
@ LLM_ERR_CONFIG_BAD_TRANSPORT
@ LLM_ERR_CONFIG_UNKNOWN_VENDOR
@ LLM_ERR_CONFIG_INLINE_SECRET
@ LLM_ERR_NOT_IMPLEMENTED
@ LLM_ERR_HTTP_UNREACHABLE
@ LLM_ERR_STREAM_CANCELLED
StreamPumpResult pumpChunkedStream(StreamByteSource &src, HTTPChunkedDecoder &dec, const std::function< void(const char *, size_t)> &onData, size_t maxIterations=100000, size_t readSize=4096)
StreamPumpResult pumpSSEStream(StreamByteSource &src, SSEEventParser &parser, const std::function< void(const std::string &)> &onEvent, size_t maxIterations=100000, size_t readSize=4096)
StreamPumpResult pumpEventStream(StreamByteSource &src, EventStreamFrameParser &parser, const std::function< void(const EventStreamFrameParser::Frame &)> &onFrame, size_t maxIterations=100000, size_t readSize=4096)
XMLAttribute getAttribute(int i=0) const
XMLNode getChildNode(int i=0) const
int nChildNode(XMLCSTR name) const