@openanonymity/nanomem 0.1.0 → 0.1.1
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.
- package/LICENSE +21 -0
- package/README.md +46 -8
- package/package.json +7 -3
- package/src/backends/BaseStorage.js +147 -3
- package/src/backends/indexeddb.js +21 -8
- package/src/browser.js +227 -0
- package/src/cli/auth.js +1 -1
- package/src/cli/commands.js +51 -8
- package/src/cli/config.js +1 -1
- package/src/cli/help.js +5 -2
- package/src/cli/output.js +4 -0
- package/src/cli.js +5 -2
- package/src/engine/deleter.js +187 -0
- package/src/engine/executors.js +416 -4
- package/src/engine/ingester.js +83 -61
- package/src/engine/recentConversation.js +110 -0
- package/src/engine/retriever.js +238 -36
- package/src/engine/toolLoop.js +51 -9
- package/src/imports/importData.js +454 -0
- package/src/imports/index.js +5 -0
- package/src/index.js +95 -2
- package/src/llm/openai.js +204 -58
- package/src/llm/tinfoil.js +508 -0
- package/src/omf.js +343 -0
- package/src/prompt_sets/conversation/ingestion.js +101 -11
- package/src/prompt_sets/document/ingestion.js +92 -4
- package/src/prompt_sets/index.js +12 -4
- package/src/types.js +133 -3
- package/src/vendor/tinfoil.browser.d.ts +2 -0
- package/src/vendor/tinfoil.browser.js +41596 -0
- package/types/backends/BaseStorage.d.ts +19 -0
- package/types/backends/indexeddb.d.ts +1 -0
- package/types/browser.d.ts +17 -0
- package/types/engine/deleter.d.ts +67 -0
- package/types/engine/executors.d.ts +54 -0
- package/types/engine/recentConversation.d.ts +18 -0
- package/types/engine/retriever.d.ts +22 -9
- package/types/imports/importData.d.ts +29 -0
- package/types/imports/index.d.ts +1 -0
- package/types/index.d.ts +9 -0
- package/types/llm/openai.d.ts +6 -9
- package/types/llm/tinfoil.d.ts +13 -0
- package/types/omf.d.ts +40 -0
- package/types/prompt_sets/conversation/ingestion.d.ts +8 -3
- package/types/prompt_sets/document/ingestion.d.ts +8 -3
- package/types/types.d.ts +125 -2
- package/types/vendor/tinfoil.browser.d.ts +6348 -0
package/src/llm/openai.js
CHANGED
|
@@ -7,6 +7,23 @@
|
|
|
7
7
|
* Uses `fetch` (built into Node 18+ and browsers).
|
|
8
8
|
*/
|
|
9
9
|
/** @import { ChatCompletionParams, ChatCompletionResponse, LLMClient, LLMClientOptions, ToolCall } from '../types.js' */
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Error & { status?: number, retryable?: boolean, retryAfterMs?: number | null, _retryFinalized?: boolean }} ApiError
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);
|
|
15
|
+
const RETRYABLE_ERROR_CODES = new Set([
|
|
16
|
+
'ECONNRESET',
|
|
17
|
+
'ECONNREFUSED',
|
|
18
|
+
'ENOTFOUND',
|
|
19
|
+
'ETIMEDOUT',
|
|
20
|
+
'EAI_AGAIN',
|
|
21
|
+
'UND_ERR_CONNECT_TIMEOUT',
|
|
22
|
+
'UND_ERR_SOCKET',
|
|
23
|
+
]);
|
|
24
|
+
const MAX_ATTEMPTS = 3;
|
|
25
|
+
const BASE_DELAY_MS = 400;
|
|
26
|
+
const MAX_DELAY_MS = 2500;
|
|
10
27
|
|
|
11
28
|
/**
|
|
12
29
|
* @param {LLMClientOptions} [options]
|
|
@@ -24,21 +41,20 @@ export function createOpenAIClient({ apiKey, baseUrl = 'https://api.openai.com/v
|
|
|
24
41
|
};
|
|
25
42
|
}
|
|
26
43
|
|
|
44
|
+
function buildRequestInit(body) {
|
|
45
|
+
return {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: buildHeaders(),
|
|
48
|
+
body: JSON.stringify(body),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
27
52
|
async function createChatCompletion({ model, messages, tools, max_tokens, temperature }) {
|
|
28
53
|
const body = { model, messages, temperature };
|
|
29
54
|
if (max_tokens != null) body.max_tokens = max_tokens;
|
|
30
55
|
if (tools && tools.length > 0) body.tools = tools;
|
|
31
56
|
|
|
32
|
-
const response = await
|
|
33
|
-
method: 'POST',
|
|
34
|
-
headers: buildHeaders(),
|
|
35
|
-
body: JSON.stringify(body),
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
if (!response.ok) {
|
|
39
|
-
const text = await response.text().catch(() => '');
|
|
40
|
-
throw new Error(`OpenAI API error ${response.status}: ${text}`);
|
|
41
|
-
}
|
|
57
|
+
const response = await fetchWithRetry(`${base}/chat/completions`, buildRequestInit(body), 'chat completion request');
|
|
42
58
|
|
|
43
59
|
const data = await response.json();
|
|
44
60
|
const choice = data.choices?.[0]?.message || {};
|
|
@@ -62,65 +78,72 @@ export function createOpenAIClient({ apiKey, baseUrl = 'https://api.openai.com/v
|
|
|
62
78
|
if (max_tokens != null) body.max_tokens = max_tokens;
|
|
63
79
|
if (tools && tools.length > 0) body.tools = tools;
|
|
64
80
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
81
|
+
return withRetry(async (attempt) => {
|
|
82
|
+
const response = await fetch(`${base}/chat/completions`, buildRequestInit(body));
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
throw await createHttpError(response, attempt);
|
|
85
|
+
}
|
|
70
86
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
87
|
+
// Only retry streaming requests if the connection dies before
|
|
88
|
+
// any SSE data arrives. Once we have surfaced deltas, replaying
|
|
89
|
+
// would duplicate partial reasoning/content.
|
|
90
|
+
let content = '';
|
|
91
|
+
let sawStreamData = false;
|
|
92
|
+
const toolCallAccumulator = new Map();
|
|
75
93
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
94
|
+
try {
|
|
95
|
+
await readSSE(response, (chunk) => {
|
|
96
|
+
sawStreamData = true;
|
|
79
97
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (!delta) return;
|
|
98
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
99
|
+
if (!delta) return;
|
|
83
100
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
101
|
+
if (delta.content) {
|
|
102
|
+
content += delta.content;
|
|
103
|
+
onDelta?.(delta.content);
|
|
104
|
+
}
|
|
89
105
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
106
|
+
if (delta.reasoning) {
|
|
107
|
+
onReasoning?.(delta.reasoning);
|
|
108
|
+
}
|
|
94
109
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
if (delta.tool_calls) {
|
|
111
|
+
for (const tc of delta.tool_calls) {
|
|
112
|
+
const idx = tc.index ?? 0;
|
|
113
|
+
if (!toolCallAccumulator.has(idx)) {
|
|
114
|
+
toolCallAccumulator.set(idx, {
|
|
115
|
+
id: tc.id || '',
|
|
116
|
+
type: 'function',
|
|
117
|
+
function: { name: '', arguments: '' },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const acc = toolCallAccumulator.get(idx);
|
|
121
|
+
if (!acc) continue;
|
|
122
|
+
if (tc.id) acc.id = tc.id;
|
|
123
|
+
if (tc.function?.name) acc.function.name += tc.function.name;
|
|
124
|
+
if (tc.function?.arguments) acc.function.arguments += tc.function.arguments;
|
|
125
|
+
}
|
|
105
126
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
127
|
+
});
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (!sawStreamData && isRetryableNetworkError(error)) {
|
|
130
|
+
const retryError = asError(error);
|
|
131
|
+
retryError.retryable = true;
|
|
132
|
+
throw retryError;
|
|
111
133
|
}
|
|
134
|
+
throw error;
|
|
112
135
|
}
|
|
113
|
-
});
|
|
114
136
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
137
|
+
const tool_calls = [...toolCallAccumulator.entries()]
|
|
138
|
+
.sort(([a], [b]) => a - b)
|
|
139
|
+
.map(([, tc]) => tc);
|
|
118
140
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
141
|
+
return {
|
|
142
|
+
content,
|
|
143
|
+
tool_calls,
|
|
144
|
+
usage: null,
|
|
145
|
+
};
|
|
146
|
+
}, 'streaming chat completion');
|
|
124
147
|
}
|
|
125
148
|
|
|
126
149
|
return { createChatCompletion, streamChatCompletion };
|
|
@@ -128,6 +151,129 @@ export function createOpenAIClient({ apiKey, baseUrl = 'https://api.openai.com/v
|
|
|
128
151
|
|
|
129
152
|
// ─── SSE Parser ──────────────────────────────────────────────
|
|
130
153
|
|
|
154
|
+
async function fetchWithRetry(url, init, context) {
|
|
155
|
+
return withRetry(async (attempt) => {
|
|
156
|
+
const response = await fetch(url, init);
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
throw await createHttpError(response, attempt);
|
|
159
|
+
}
|
|
160
|
+
return response;
|
|
161
|
+
}, context);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function withRetry(fn, context) {
|
|
165
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
|
|
166
|
+
try {
|
|
167
|
+
return await fn(attempt);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
const normalized = asError(error);
|
|
170
|
+
const shouldRetry = attempt < MAX_ATTEMPTS && isRetryableError(normalized);
|
|
171
|
+
if (!shouldRetry) {
|
|
172
|
+
throw finalizeRetryError(normalized, attempt);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const delay = getRetryDelay(attempt - 1, normalized.retryAfterMs || null);
|
|
176
|
+
console.warn(`[nanomem/openai] ${context} attempt ${attempt}/${MAX_ATTEMPTS} failed: ${normalized.message}. Retrying in ${Math.round(delay)}ms.`);
|
|
177
|
+
await sleep(delay);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
throw new Error(`OpenAI API ${context} failed after ${MAX_ATTEMPTS} attempts.`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isRetryableError(error) {
|
|
185
|
+
if (!error) return false;
|
|
186
|
+
if (error.retryable === true) return true;
|
|
187
|
+
if (typeof error.status === 'number') {
|
|
188
|
+
return RETRYABLE_STATUS.has(error.status);
|
|
189
|
+
}
|
|
190
|
+
return isRetryableNetworkError(error);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function isRetryableNetworkError(error) {
|
|
194
|
+
if (!error || error.isUserAbort) return false;
|
|
195
|
+
if (error.name === 'TypeError' || error.name === 'AbortError') return true;
|
|
196
|
+
|
|
197
|
+
const code = String(error.code || error.cause?.code || '').toUpperCase();
|
|
198
|
+
if (RETRYABLE_ERROR_CODES.has(code)) return true;
|
|
199
|
+
|
|
200
|
+
const message = String(error.message || '').toLowerCase();
|
|
201
|
+
return message.includes('failed to fetch')
|
|
202
|
+
|| message.includes('network')
|
|
203
|
+
|| message.includes('timeout')
|
|
204
|
+
|| message.includes('err_network_changed')
|
|
205
|
+
|| message.includes('econnreset')
|
|
206
|
+
|| message.includes('connection');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @param {number} attempt
|
|
211
|
+
* @param {number | null} [retryAfterMs]
|
|
212
|
+
* @returns {number}
|
|
213
|
+
*/
|
|
214
|
+
function getRetryDelay(attempt, retryAfterMs = null) {
|
|
215
|
+
if (retryAfterMs != null && Number.isFinite(retryAfterMs) && retryAfterMs > 0) {
|
|
216
|
+
return Math.min(retryAfterMs, MAX_DELAY_MS);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const exponential = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
220
|
+
const jitter = Math.random() * BASE_DELAY_MS;
|
|
221
|
+
return Math.min(exponential + jitter, MAX_DELAY_MS);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function sleep(ms) {
|
|
225
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function createHttpError(response, attempt = 1) {
|
|
229
|
+
const text = await response.text().catch(() => '');
|
|
230
|
+
const suffix = attempt > 1 ? ` after ${attempt} attempts` : '';
|
|
231
|
+
const error = /** @type {ApiError} */ (new Error(`OpenAI API error ${response.status}${suffix}: ${text}`));
|
|
232
|
+
error.status = response.status;
|
|
233
|
+
error.retryable = RETRYABLE_STATUS.has(response.status);
|
|
234
|
+
error.retryAfterMs = parseRetryAfterMs(response);
|
|
235
|
+
return error;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function parseRetryAfterMs(response) {
|
|
239
|
+
const value = response?.headers?.get?.('Retry-After');
|
|
240
|
+
if (!value) return null;
|
|
241
|
+
|
|
242
|
+
const seconds = Number.parseInt(value, 10);
|
|
243
|
+
if (Number.isFinite(seconds) && seconds > 0) {
|
|
244
|
+
return seconds * 1000;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const date = Date.parse(value);
|
|
248
|
+
if (Number.isFinite(date)) {
|
|
249
|
+
const ms = date - Date.now();
|
|
250
|
+
return ms > 0 ? ms : null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function finalizeRetryError(error, attempts) {
|
|
257
|
+
const normalized = asError(error);
|
|
258
|
+
if (attempts <= 1 || normalized._retryFinalized) {
|
|
259
|
+
return normalized;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!normalized.message.includes('after ')) {
|
|
263
|
+
normalized.message = `${normalized.message} (after ${attempts} attempts)`;
|
|
264
|
+
}
|
|
265
|
+
normalized._retryFinalized = true;
|
|
266
|
+
return normalized;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* @param {unknown} error
|
|
271
|
+
* @returns {ApiError}
|
|
272
|
+
*/
|
|
273
|
+
function asError(error) {
|
|
274
|
+
return /** @type {ApiError} */ (error instanceof Error ? error : new Error(String(error)));
|
|
275
|
+
}
|
|
276
|
+
|
|
131
277
|
async function readSSE(response, onMessage) {
|
|
132
278
|
if (!response.body) {
|
|
133
279
|
throw new Error('Streaming response body is not available.');
|