CMSDK 2.0.1
Cross-platform C++ base library and SDK for the Psyclone AIOS platform
Loading...
Searching...
No Matches
LLMConnection.h
Go to the documentation of this file.
1#if !defined(_LLMCONNECTION_H_)
2#define _LLMCONNECTION_H_
3
4#pragma once
5
6#include "Utils.h"
7#include "xml_parser.h"
8
9#include <string>
10#include <vector>
11
12namespace cmlabs{
13
14// ============================================================================
15// LLMConnection (T1.1 step 1.1a)
16//
17// Named, node-owned connection object to an LLM endpoint, configured from
18// the reserved <llm> named-connection XML element (D1/F1.8) which contains
19// an <llmvendor> vendor-descriptor sub-block:
20//
21// <llm name="main" apikeyref="OPENAI_API_KEY" global="true" schemaversion="1"
22// maxtokensperrequest="4096" maxrequestspermin="60"
23// maxconcurrent="2" costceiling="10.0">
24// <llmvendor type="openai" endpoint="https://api.openai.com/v1/chat/completions">
25// <header name="Authorization" value="Bearer %apikey%"/>
26// <requesttemplate>{"model":"%model%","messages":[...]}</requesttemplate>
27// <responsetemplate>choices[0].message.content</responsetemplate>
28// <models default="gpt-4o">
29// <model name="gpt-4o"/>
30// <model name="gpt-4o-mini"/>
31// </models>
32// </llmvendor>
33// </llm>
34//
35// Secrets are NEVER inline in the XML (Q-C). `apikeyref` is an indirect
36// reference, resolved at configure() time:
37// apikeyref="env:NAME" -> environment variable NAME (set, non-empty)
38// apikeyref="file:/path" -> whole file contents, trailing newline/
39// whitespace trimmed (single-line key files)
40// apikeyref="NAME" -> legacy bare form: env var NAME, else a
41// key=value entry in optional `secretsfile`
42// A missing env var or unreadable file fails typed at connect() with
43// LLM_ERR_SECRET_UNRESOLVED; the secret value (and the failure detail)
44// never appears in logs, errors, or getSystemStatus(). An inline `apikey`
45// attribute or <apikey> child is a typed configuration error
46// (LLM_ERR_CONFIG_INLINE_SECRET).
47//
48// C13 constraint fields (max tokens/request, max requests/min, max
49// concurrent, optional cost ceiling) are parsed and stored only — no
50// enforcement logic in this step.
51//
52// Networking mode A (blocking request/reply over NetworkManager::
53// makeHTTPRequest) is implemented in step 1.1b: connect() validates config +
54// resolved secret and marks the connection live (endpoint reachability is
55// checked lazily, on the first interact() — the underlying HTTP client is
56// connectionless/blocking, so there is nothing to dial eagerly); interact()
57// shapes the request from <requesttemplate> (%model%/%input%) and the
58// configured headers (%apikey%/%model%), sends it, and extracts the reply
59// text via the <responsetemplate> JSON path. reset() drops the live state
60// but retains config and the final stats record (ended=true). Streaming
61// (mode B) is step 1.2; constraint ENFORCEMENT is a later step.
62// ============================================================================
63
64// Typed result codes for all LLMConnection operations.
66 LLM_OK = 0,
67 LLM_ERR_NOT_CONFIGURED, // API used before a successful configure()
68 LLM_ERR_NOT_CONNECTED, // no live connection (networking is step 1.1b)
69 LLM_ERR_NOT_IMPLEMENTED, // behaviour deferred to a later step
70 LLM_ERR_CONFIG_EMPTY, // missing/empty <llm> element
71 LLM_ERR_CONFIG_NOT_LLM, // element is not an <llm> element
72 LLM_ERR_CONFIG_PARSE, // XML string did not parse
73 LLM_ERR_CONFIG_NO_NAME, // <llm> is a NAMED connection: name required
74 LLM_ERR_CONFIG_NO_VENDOR, // missing <llmvendor> sub-block
75 LLM_ERR_CONFIG_UNKNOWN_VENDOR, // unrecognised <llmvendor type="...">
76 LLM_ERR_CONFIG_NO_ENDPOINT, // missing endpoint URL
77 LLM_ERR_CONFIG_INLINE_SECRET, // inline apikey in XML — use apikeyref
78 LLM_ERR_CONFIG_INVALID, // other structural configuration error
79 LLM_ERR_UNKNOWN_MODEL, // selectModel() name not in the model list
80 LLM_ERR_SECRET_UNRESOLVED, // apikeyref set but no secret could be resolved
81 LLM_ERR_NO_INPUT, // interact() called with empty input
82 LLM_ERR_HTTP_UNREACHABLE, // endpoint unreachable / no HTTP reply
83 LLM_ERR_HTTP, // endpoint replied with a non-200 status
84 LLM_ERR_REPLY_PARSE, // reply body missing/malformed vs responsetemplate
85 LLM_ERR_CONFIG_BAD_TRANSPORT, // unrecognised <llmvendor transport="...">
86 LLM_ERR_STREAM_CANCELLED // streaming interact stopped early by the sink
87};
88
89// Human-readable name for an LLMResult (for logs and typed-error reporting).
90const char* LLMResultText(LLMResult result);
91
92// Vendor family recognised from <llmvendor type="...">. "custom" is allowed
93// for config-only vendors that fully specify their own HTTP shape.
102
103const char* LLMVendorTypeText(LLMVendorType type);
104
105// Streaming wire format used by mode B (T1.1 step 1.2c). Resolved ONCE at
106// configure() time from <llmvendor transport="sse|eventstream|chunked">, or
107// by vendor default when the attribute is absent (bedrock -> eventstream,
108// everything else -> sse). Callers never branch on vendor: they just call
109// interactStream() and the connection drives the right decoder internally.
111 LLM_STREAM_NONE = 0, // unconfigured / vendor does not stream
112 LLM_STREAM_SSE, // chunked transfer + text/event-stream SSE events
113 LLM_STREAM_EVENTSTREAM, // AWS/Bedrock binary eventstream framing
114 LLM_STREAM_CHUNKED // raw chunked-transfer body, no event framing
115};
116
117const char* LLMStreamTransportText(LLMStreamTransport transport);
118
119// Sink for incremental streaming output (mode B). Return true to continue,
120// false to cancel the stream early (interactStream() then returns
121// LLM_ERR_STREAM_CANCELLED). `token` is the decoded incremental text (NOT
122// raw wire bytes); `userData` is the pointer given to interactStream().
123typedef bool (*LLMStreamCallback)(const char* token, uint32 size, void* userData);
124
125// One configured HTTP header (value may contain %apikey%/%model% templates).
126struct LLMHeader {
127 std::string name;
128 std::string value;
129};
130
131// C13 per-connection constraints — parsed and stored in this step, enforced
132// in a later step. Values <= 0 (or < 0 for costCeiling) mean "not set".
142
143// Connection statistics (uptime, bytes, tokens, cost). All-zero until
144// networking lands in later steps; retained after disconnect (ended=true).
145struct LLMStats {
146 uint64 uptimeMS;
147 uint64 bytesSent;
150 uint64 tokensIn;
151 uint64 tokensOut;
152 uint64 replyChunks; // streamed token chunks delivered (mode B)
153 double cost;
154 bool ended;
155
158 cost(0.0), ended(false) {}
159};
160
161// 1.2d-i: owner-notification hook. The owning Node registers a callback so it
162// is told when this connection ends (reset() or destruction) and can retain a
163// final stats snapshot without holding a dangling pointer. Plain C callback to
164// keep CMSDK independent of the Psyclone Node type.
165class LLMConnection;
166typedef void (*LLMOwnerEndCallback)(void* owner, LLMConnection* connection,
167 const LLMStats& finalStats);
168
170public:
173
174 // ---- Ownership (1.2d-i) --------------------------------------------------
175 // Set (or clear, with NULLs) the owner-end callback; invoked at most once
176 // per connect()..end cycle, from reset() or the destructor.
177 void setOwner(void* owner, LLMOwnerEndCallback callback);
178
179 // ---- Configuration -----------------------------------------------------
180 // Parse an <llm> element (containing an <llmvendor> sub-block). On any
181 // error the connection stays/becomes unconfigured and a typed error is
182 // returned; bad config must never crash.
183 LLMResult configure(const XMLNode& llmNode);
184 // Convenience: parse the <llm> element from an XML string first.
185 LLMResult configureFromString(const char* xml);
186
187 bool isConfigured() const { return configured; }
188 LLMResult getLastError() const { return lastError; }
189
190 // ---- Parsed fields -----------------------------------------------------
191 const std::string& getName() const { return name; }
192 LLMVendorType getVendorType() const { return vendorType; }
193 const std::string& getVendorTypeName() const { return vendorTypeName; }
194 const std::string& getEndpoint() const { return endpoint; }
195 bool isGlobal() const { return globalConnection; }
196 const std::string& getSchemaVersion() const { return schemaVersion; }
197 const std::string& getAPIKeyRef() const { return apiKeyRef; }
198 // True if apikeyref resolved to a secret (env var or secretsfile entry).
199 // The secret value itself is never exposed through the public API.
200 bool hasResolvedSecret() const { return !apiKeySecret.empty(); }
201 const std::string& getRequestTemplate() const { return requestTemplate; }
202 const std::string& getResponseTemplate() const { return responseTemplate; }
203 const std::vector<LLMHeader>& getHeaders() const { return headers; }
204 const LLMConstraints& getConstraints() const { return constraints; }
205
206 // ---- Models ------------------------------------------------------------
207 // Model names from the parsed <models> list (empty if unconfigured).
208 std::vector<std::string> listModels() const;
209 // Select a model by name from the parsed list; unknown -> typed error.
210 LLMResult selectModel(const char* modelName);
211 const std::string& getSelectedModel() const { return selectedModel; }
212
213 // ---- Networking (mode A, step 1.1b) -------------------------------------
214 // Validate config + secret and mark connected; starts a fresh stats record.
215 // Reachability is verified lazily by the first interact().
217 // Drop the live connection; config is kept, stats retained with ended=true.
219 // One blocking request/reply exchange (mode A). On success `reply` holds
220 // the text extracted via the responsetemplate JSON path.
221 LLMResult interact(const char* input, std::string& reply);
222 // Streaming request (mode B, step 1.2c): sends one request and delivers
223 // incremental decoded tokens to `callback` as they arrive; on success
224 // `fullReply` (if non-NULL) holds the assembled full text. The wire
225 // decoder (SSE / eventstream / chunked) is chosen from the configured
226 // stream transport — callers never branch on vendor. Blocking until the
227 // stream completes, errors, or the callback cancels.
228 LLMResult interactStream(const char* input, LLMStreamCallback callback,
229 void* userData, std::string* fullReply = NULL);
230 bool isConnected() const { return connected; }
231 // Transport resolved at configure() time (LLM_STREAM_NONE if unconfigured).
232 LLMStreamTransport getStreamTransport() const { return streamTransport; }
233
234 // ---- Stats -------------------------------------------------------------
235 LLMStats stats() const;
236
237private:
238 LLMResult fail(LLMResult result);
239 LLMResult interactFail(LLMResult result);
240 std::string applyTemplates(const std::string& text, const char* input) const;
241 // Extract a dotted/indexed JSON path (e.g. choices[0].message.content).
242 static bool extractJSONPath(const char* json, uint32 size, const char* path,
243 std::string& out);
244 void clearConfig();
245 LLMResult parseVendor(const XMLNode& vendorNode);
246 // Resolve transport from an explicit attribute value (may be NULL/empty)
247 // + the already-parsed vendorType default. Sets streamTransport.
248 LLMResult resolveStreamTransport(const char* transportAttr);
249 void resolveSecret();
250 static bool nodeHasInlineSecret(const XMLNode& node);
251 // 1.2c-ii: reads the HTTP reply header off `con`, then drives the
252 // transport decoder, delivering token text to callback (con is a
253 // NetworkConnection*, typed void* to keep networking out of this header).
254 LLMResult pumpStreamReply(void* con, LLMStreamCallback callback,
255 void* userData, std::string* fullReply);
256
257 bool configured;
258 bool connected;
259 LLMResult lastError;
260
261 std::string name;
262 LLMVendorType vendorType;
263 std::string vendorTypeName;
264 std::string endpoint;
265 LLMStreamTransport streamTransport;
266 bool globalConnection;
267 std::string schemaVersion;
268 std::string apiKeyRef;
269 std::string secretsFile;
270 std::string apiKeySecret; // resolved secret — never logged/exposed
271 std::string requestTemplate;
272 std::string responseTemplate;
273 std::vector<LLMHeader> headers;
274 LLMConstraints constraints;
275
276 std::vector<std::string> models;
277 std::string selectedModel;
278
279 // Extract vendor usage token counts from one reply/event JSON body into
280 // currentStats (mode A whole reply and mode B per-event share this).
281 void accountUsageJSON(const char* json, uint32 size);
282
283 // Invoke the owner-end callback (if set and not yet fired for this cycle).
284 void notifyOwnerEnded();
285
286 void* owner; // opaque owner handle (1.2d-i)
287 LLMOwnerEndCallback ownerEndCallback;
288 bool ownerNotified; // callback fired for current cycle
289
290 LLMStats currentStats;
291 volatile bool cancelRequested; // set by reset() to abort a live stream
292 uint64 connectTimeMS; // wall-clock ms at connect() (0 = never)
293 class NetworkManager* netManager; // owned; created on connect()
294};
295
296} // namespace cmlabs
297
298#endif // !defined(_LLMCONNECTION_H_)
Cross-platform utility toolbox for CMSDK: threading, synchronization, shared memory,...
const std::string & getVendorTypeName() const
LLMResult getLastError() const
const std::string & getName() const
LLMResult interact(const char *input, std::string &reply)
LLMResult configure(const XMLNode &llmNode)
const std::string & getSelectedModel() const
const std::vector< LLMHeader > & getHeaders() const
void setOwner(void *owner, LLMOwnerEndCallback callback)
LLMResult selectModel(const char *modelName)
LLMResult configureFromString(const char *xml)
const std::string & getEndpoint() const
const LLMConstraints & getConstraints() const
LLMVendorType getVendorType() const
LLMResult interactStream(const char *input, LLMStreamCallback callback, void *userData, std::string *fullReply=NULL)
LLMStreamTransport getStreamTransport() const
std::vector< std::string > listModels() const
const std::string & getSchemaVersion() const
bool hasResolvedSecret() const
const std::string & getResponseTemplate() const
const std::string & getRequestTemplate() const
bool isConfigured() const
const std::string & getAPIKeyRef() const
LLMStats stats() const
Central owner of all channels, listeners and connections in a process.
@ LLM_STREAM_EVENTSTREAM
@ LLM_STREAM_NONE
@ LLM_STREAM_CHUNKED
@ LLM_VENDOR_CUSTOM
@ LLM_VENDOR_GOOGLE
@ LLM_VENDOR_OPENAI
@ LLM_VENDOR_NONE
@ LLM_VENDOR_BEDROCK
@ LLM_VENDOR_ANTHROPIC
const char * LLMResultText(LLMResult result)
const char * LLMStreamTransportText(LLMStreamTransport transport)
const char * LLMVendorTypeText(LLMVendorType type)
bool(* LLMStreamCallback)(const char *token, uint32 size, void *userData)
void(* LLMOwnerEndCallback)(void *owner, LLMConnection *connection, const LLMStats &finalStats)
@ LLM_ERR_CONFIG_EMPTY
@ LLM_ERR_CONFIG_NO_VENDOR
@ LLM_ERR_CONFIG_INVALID
@ LLM_ERR_CONFIG_NO_ENDPOINT
@ LLM_ERR_NOT_CONNECTED
@ LLM_ERR_CONFIG_PARSE
@ LLM_ERR_SECRET_UNRESOLVED
@ LLM_ERR_CONFIG_BAD_TRANSPORT
@ LLM_ERR_CONFIG_NOT_LLM
@ LLM_ERR_CONFIG_UNKNOWN_VENDOR
@ LLM_ERR_CONFIG_INLINE_SECRET
@ LLM_ERR_UNKNOWN_MODEL
@ LLM_ERR_NOT_IMPLEMENTED
@ LLM_ERR_NOT_CONFIGURED
@ LLM_ERR_CONFIG_NO_NAME
@ LLM_ERR_HTTP_UNREACHABLE
@ LLM_ERR_REPLY_PARSE
@ LLM_ERR_STREAM_CANCELLED
@ LLM_ERR_NO_INPUT
Small recursive XML DOM parser (XMLNode) used by CMSDK for all PsySpec XML parsing.