@shadowforge0/aquifer-memory 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,119 @@
1
+ 'use strict';
2
+
3
+ const http = require('http');
4
+ const https = require('https');
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // HTTP helper (same pattern as pipeline/embed.js)
8
+ // ---------------------------------------------------------------------------
9
+
10
+ function httpRequest(url, options, body) {
11
+ return new Promise((resolve, reject) => {
12
+ const parsedUrl = new URL(url);
13
+ const transport = parsedUrl.protocol === 'https:' ? https : http;
14
+
15
+ let settled = false;
16
+ const finish = (fn, val) => { if (!settled) { settled = true; fn(val); } };
17
+
18
+ const req = transport.request(parsedUrl, options, (res) => {
19
+ const chunks = [];
20
+ res.on('data', (chunk) => chunks.push(chunk));
21
+ res.on('end', () => {
22
+ if (timer) clearTimeout(timer);
23
+ const raw = Buffer.concat(chunks).toString();
24
+ if (res.statusCode < 200 || res.statusCode >= 300) {
25
+ // Truncate error body to avoid leaking prompt content from echo-back proxies
26
+ const safeBody = raw.slice(0, 200).replace(/[\n\r]/g, ' ');
27
+ const err = new Error(`LLM HTTP ${res.statusCode}: ${safeBody}`);
28
+ err.statusCode = res.statusCode;
29
+ finish(reject, err);
30
+ return;
31
+ }
32
+ try {
33
+ finish(resolve, JSON.parse(raw));
34
+ } catch (e) {
35
+ finish(reject, new Error(`Invalid JSON from LLM (${raw.length} bytes)`));
36
+ }
37
+ });
38
+ });
39
+
40
+ const timer = options.timeout
41
+ ? setTimeout(() => { req.destroy(); finish(reject, new Error('LLM request timeout')); }, options.timeout)
42
+ : null;
43
+
44
+ req.on('error', (e) => { if (timer) clearTimeout(timer); finish(reject, e); });
45
+ if (body) req.write(body);
46
+ req.end();
47
+ });
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Retry with exponential backoff
52
+ // ---------------------------------------------------------------------------
53
+
54
+ const RETRYABLE_CODES = new Set([408, 429, 500, 502, 503, 504]);
55
+
56
+ async function withRetry(fn, { maxRetries = 3, initialBackoffMs = 2000 } = {}) {
57
+ let lastErr;
58
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
59
+ if (attempt > 0) {
60
+ await new Promise(r => setTimeout(r, initialBackoffMs * Math.pow(2, attempt - 1)));
61
+ }
62
+ try {
63
+ return await fn();
64
+ } catch (err) {
65
+ lastErr = err;
66
+ // Don't retry non-retryable HTTP errors or timeouts
67
+ if (err.statusCode && !RETRYABLE_CODES.has(err.statusCode)) throw err;
68
+ if (err.message === 'LLM request timeout') throw err;
69
+ }
70
+ }
71
+ throw lastErr;
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // createLlmFn
76
+ // ---------------------------------------------------------------------------
77
+
78
+ function createLlmFn(config = {}) {
79
+ const baseUrl = config.baseUrl;
80
+ const model = config.model;
81
+ const apiKey = config.apiKey || null;
82
+ const timeoutMs = config.timeoutMs || 60000;
83
+ const maxRetries = config.maxRetries || 3;
84
+ const initialBackoffMs = config.initialBackoffMs || 2000;
85
+ const temperature = config.temperature ?? 0;
86
+
87
+ if (!baseUrl) throw new Error('LLM config requires baseUrl');
88
+ if (!model) throw new Error('LLM config requires model');
89
+
90
+ const endpoint = baseUrl.replace(/\/+$/, '') + '/chat/completions';
91
+
92
+ return async function llmFn(prompt) {
93
+ const body = JSON.stringify({
94
+ model,
95
+ temperature,
96
+ messages: [{ role: 'user', content: prompt }],
97
+ });
98
+
99
+ const headers = { 'Content-Type': 'application/json' };
100
+ if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
101
+
102
+ const data = await withRetry(
103
+ () => httpRequest(endpoint, {
104
+ method: 'POST',
105
+ headers,
106
+ timeout: timeoutMs,
107
+ }, body),
108
+ { maxRetries, initialBackoffMs }
109
+ );
110
+
111
+ const content = data.choices?.[0]?.message?.content;
112
+ if (typeof content !== 'string') {
113
+ throw new Error('LLM response missing choices[0].message.content');
114
+ }
115
+ return content;
116
+ };
117
+ }
118
+
119
+ module.exports = { createLlmFn };