@yeaft/webchat-agent 0.1.565 → 0.1.568
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/package.json +1 -1
- package/unify/llm/adapter.js +52 -0
- package/unify/llm/anthropic.js +30 -4
- package/unify/llm/chat-completions.js +28 -4
package/package.json
CHANGED
package/unify/llm/adapter.js
CHANGED
|
@@ -103,6 +103,58 @@ export class LLMAbortError extends Error {
|
|
|
103
103
|
|
|
104
104
|
// ─── task-344: Raw payload redaction helper ────────────────────
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* task-344 follow-up (N2): cap raw payload size exposed via onRawExchange.
|
|
108
|
+
* Prevents the web debug store from linear-growing when a single turn
|
|
109
|
+
* sends / receives megabytes of content. 256 KiB per field per turn.
|
|
110
|
+
*/
|
|
111
|
+
export const RAW_PAYLOAD_CAP_BYTES = 256 * 1024;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Truncate a string to at most `cap` bytes (UTF-8). When within cap the
|
|
115
|
+
* original is returned; otherwise a prefix with a trailing
|
|
116
|
+
* `…[truncated, original N bytes]` marker. Non-string inputs pass through.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} s
|
|
119
|
+
* @param {number} [cap=RAW_PAYLOAD_CAP_BYTES]
|
|
120
|
+
* @returns {string}
|
|
121
|
+
*/
|
|
122
|
+
export function capRawString(s, cap = RAW_PAYLOAD_CAP_BYTES) {
|
|
123
|
+
if (typeof s !== 'string') return s;
|
|
124
|
+
const encoder = new TextEncoder();
|
|
125
|
+
const fullBytes = encoder.encode(s);
|
|
126
|
+
if (fullBytes.length <= cap) return s;
|
|
127
|
+
const decoder = new TextDecoder('utf-8', { fatal: false });
|
|
128
|
+
const prefix = decoder.decode(fullBytes.slice(0, cap));
|
|
129
|
+
return `${prefix}…[truncated, original ${fullBytes.length} bytes]`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Cap the `body` field of a rawRequest envelope. JSON-stringifies objects
|
|
134
|
+
* before sizing so an oversized `messages` array does not escape the cap.
|
|
135
|
+
* When truncation fires, body becomes a string (JSON prefix + marker);
|
|
136
|
+
* objects under cap stay objects.
|
|
137
|
+
*
|
|
138
|
+
* @param {{ url: string, method: string, headers: object, body: any }} req
|
|
139
|
+
* @param {number} [cap=RAW_PAYLOAD_CAP_BYTES]
|
|
140
|
+
* @returns {{ url: string, method: string, headers: object, body: any }}
|
|
141
|
+
*/
|
|
142
|
+
export function capRawRequest(req, cap = RAW_PAYLOAD_CAP_BYTES) {
|
|
143
|
+
if (!req || typeof req !== 'object') return req;
|
|
144
|
+
let body = req.body;
|
|
145
|
+
if (body != null && typeof body !== 'string') {
|
|
146
|
+
let serialized;
|
|
147
|
+
try { serialized = JSON.stringify(body); }
|
|
148
|
+
catch { serialized = String(body); }
|
|
149
|
+
if (typeof serialized === 'string' && new TextEncoder().encode(serialized).length > cap) {
|
|
150
|
+
body = capRawString(serialized, cap);
|
|
151
|
+
}
|
|
152
|
+
} else if (typeof body === 'string') {
|
|
153
|
+
body = capRawString(body, cap);
|
|
154
|
+
}
|
|
155
|
+
return { url: req.url, method: req.method, headers: req.headers, body };
|
|
156
|
+
}
|
|
157
|
+
|
|
106
158
|
/**
|
|
107
159
|
* Redact sensitive headers (API keys / bearer tokens) from a raw request
|
|
108
160
|
* shape before exposing it to debug UI. Always returns a NEW object — never
|
package/unify/llm/anthropic.js
CHANGED
|
@@ -14,6 +14,9 @@ import {
|
|
|
14
14
|
LLMServerError,
|
|
15
15
|
LLMAbortError,
|
|
16
16
|
redactRawRequest,
|
|
17
|
+
capRawRequest,
|
|
18
|
+
capRawString,
|
|
19
|
+
RAW_PAYLOAD_CAP_BYTES,
|
|
17
20
|
} from './adapter.js';
|
|
18
21
|
import {
|
|
19
22
|
normalizeEffort,
|
|
@@ -182,7 +185,8 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
182
185
|
};
|
|
183
186
|
|
|
184
187
|
// task-344: expose raw request (redacted) for debug panel.
|
|
185
|
-
|
|
188
|
+
// task-344 follow-up (N2): cap body to RAW_PAYLOAD_CAP_BYTES.
|
|
189
|
+
const rawRequest = capRawRequest(redactRawRequest({ url, method: 'POST', headers, body }));
|
|
186
190
|
|
|
187
191
|
const response = await fetch(url, {
|
|
188
192
|
method: 'POST',
|
|
@@ -203,7 +207,8 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
203
207
|
headers: response.headers && typeof response.headers.entries === 'function'
|
|
204
208
|
? Object.fromEntries(response.headers.entries())
|
|
205
209
|
: {},
|
|
206
|
-
|
|
210
|
+
// task-344 follow-up (N2): cap error body.
|
|
211
|
+
body: capRawString(errorBody),
|
|
207
212
|
},
|
|
208
213
|
});
|
|
209
214
|
} catch { /* ignore */ }
|
|
@@ -219,7 +224,11 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
219
224
|
let currentToolName = null;
|
|
220
225
|
let currentToolInput = '';
|
|
221
226
|
// task-344: accumulate raw SSE body for debug exposure.
|
|
227
|
+
// task-344 follow-up (N2): cap growth — once past RAW_PAYLOAD_CAP_BYTES
|
|
228
|
+
// we freeze the captured body and record total-bytes-seen separately.
|
|
222
229
|
let rawSseBody = '';
|
|
230
|
+
let rawSseTotalBytes = 0;
|
|
231
|
+
let rawSseCapped = false;
|
|
223
232
|
const responseHeaders = response.headers && typeof response.headers.entries === 'function'
|
|
224
233
|
? Object.fromEntries(response.headers.entries())
|
|
225
234
|
: {};
|
|
@@ -232,7 +241,20 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
232
241
|
|
|
233
242
|
const chunkText = decoder.decode(value, { stream: true });
|
|
234
243
|
buffer += chunkText;
|
|
235
|
-
|
|
244
|
+
// task-344 follow-up (N2): size-capped capture.
|
|
245
|
+
rawSseTotalBytes += value.byteLength;
|
|
246
|
+
if (!rawSseCapped) {
|
|
247
|
+
if (rawSseTotalBytes <= RAW_PAYLOAD_CAP_BYTES) {
|
|
248
|
+
rawSseBody += chunkText;
|
|
249
|
+
} else {
|
|
250
|
+
// Append enough of this chunk to meet the cap, then freeze.
|
|
251
|
+
const remaining = RAW_PAYLOAD_CAP_BYTES - (rawSseTotalBytes - value.byteLength);
|
|
252
|
+
if (remaining > 0) {
|
|
253
|
+
rawSseBody += chunkText.slice(0, remaining);
|
|
254
|
+
}
|
|
255
|
+
rawSseCapped = true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
236
258
|
const lines = buffer.split('\n');
|
|
237
259
|
buffer = lines.pop() || ''; // Keep incomplete line
|
|
238
260
|
|
|
@@ -325,12 +347,16 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
325
347
|
// task-344: emit raw exchange after stream completes (or errors).
|
|
326
348
|
if (onRawExchange) {
|
|
327
349
|
try {
|
|
350
|
+
// task-344 follow-up (N2): append truncation marker when capped.
|
|
351
|
+
const finalBody = rawSseCapped
|
|
352
|
+
? `${rawSseBody}…[truncated, original ${rawSseTotalBytes} bytes]`
|
|
353
|
+
: rawSseBody;
|
|
328
354
|
onRawExchange({
|
|
329
355
|
rawRequest,
|
|
330
356
|
rawResponse: {
|
|
331
357
|
status: responseStatus,
|
|
332
358
|
headers: responseHeaders,
|
|
333
|
-
body:
|
|
359
|
+
body: finalBody,
|
|
334
360
|
format: 'sse',
|
|
335
361
|
},
|
|
336
362
|
});
|
|
@@ -28,6 +28,9 @@ import {
|
|
|
28
28
|
LLMServerError,
|
|
29
29
|
LLMAbortError,
|
|
30
30
|
redactRawRequest,
|
|
31
|
+
capRawRequest,
|
|
32
|
+
capRawString,
|
|
33
|
+
RAW_PAYLOAD_CAP_BYTES,
|
|
31
34
|
} from './adapter.js';
|
|
32
35
|
import {
|
|
33
36
|
normalizeEffort,
|
|
@@ -239,7 +242,8 @@ export class ChatCompletionsAdapter extends LLMAdapter {
|
|
|
239
242
|
};
|
|
240
243
|
|
|
241
244
|
// task-344: expose raw request (redacted) for debug panel.
|
|
242
|
-
|
|
245
|
+
// task-344 follow-up (N2): cap body size.
|
|
246
|
+
const rawRequest = capRawRequest(redactRawRequest({ url, method: 'POST', headers, body }));
|
|
243
247
|
|
|
244
248
|
const response = await fetch(url, {
|
|
245
249
|
method: 'POST',
|
|
@@ -259,7 +263,8 @@ export class ChatCompletionsAdapter extends LLMAdapter {
|
|
|
259
263
|
headers: response.headers && typeof response.headers.entries === 'function'
|
|
260
264
|
? Object.fromEntries(response.headers.entries())
|
|
261
265
|
: {},
|
|
262
|
-
|
|
266
|
+
// task-344 follow-up (N2): cap error body.
|
|
267
|
+
body: capRawString(errorBody),
|
|
263
268
|
},
|
|
264
269
|
});
|
|
265
270
|
} catch { /* ignore */ }
|
|
@@ -272,7 +277,10 @@ export class ChatCompletionsAdapter extends LLMAdapter {
|
|
|
272
277
|
const decoder = new TextDecoder();
|
|
273
278
|
let buffer = '';
|
|
274
279
|
// task-344: accumulate raw SSE body + headers/status for debug exposure.
|
|
280
|
+
// task-344 follow-up (N2): cap growth at RAW_PAYLOAD_CAP_BYTES.
|
|
275
281
|
let rawSseBody = '';
|
|
282
|
+
let rawSseTotalBytes = 0;
|
|
283
|
+
let rawSseCapped = false;
|
|
276
284
|
const responseHeaders = response.headers && typeof response.headers.entries === 'function'
|
|
277
285
|
? Object.fromEntries(response.headers.entries())
|
|
278
286
|
: {};
|
|
@@ -290,7 +298,19 @@ export class ChatCompletionsAdapter extends LLMAdapter {
|
|
|
290
298
|
|
|
291
299
|
const chunkText = decoder.decode(value, { stream: true });
|
|
292
300
|
buffer += chunkText;
|
|
293
|
-
|
|
301
|
+
// task-344 follow-up (N2): size-capped capture.
|
|
302
|
+
rawSseTotalBytes += value.byteLength;
|
|
303
|
+
if (!rawSseCapped) {
|
|
304
|
+
if (rawSseTotalBytes <= RAW_PAYLOAD_CAP_BYTES) {
|
|
305
|
+
rawSseBody += chunkText;
|
|
306
|
+
} else {
|
|
307
|
+
const remaining = RAW_PAYLOAD_CAP_BYTES - (rawSseTotalBytes - value.byteLength);
|
|
308
|
+
if (remaining > 0) {
|
|
309
|
+
rawSseBody += chunkText.slice(0, remaining);
|
|
310
|
+
}
|
|
311
|
+
rawSseCapped = true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
294
314
|
const lines = buffer.split('\n');
|
|
295
315
|
buffer = lines.pop() || '';
|
|
296
316
|
|
|
@@ -377,12 +397,16 @@ export class ChatCompletionsAdapter extends LLMAdapter {
|
|
|
377
397
|
// task-344: emit raw exchange after stream completes.
|
|
378
398
|
if (onRawExchange) {
|
|
379
399
|
try {
|
|
400
|
+
// task-344 follow-up (N2): append truncation marker when capped.
|
|
401
|
+
const finalBody = rawSseCapped
|
|
402
|
+
? `${rawSseBody}…[truncated, original ${rawSseTotalBytes} bytes]`
|
|
403
|
+
: rawSseBody;
|
|
380
404
|
onRawExchange({
|
|
381
405
|
rawRequest,
|
|
382
406
|
rawResponse: {
|
|
383
407
|
status: responseStatus,
|
|
384
408
|
headers: responseHeaders,
|
|
385
|
-
body:
|
|
409
|
+
body: finalBody,
|
|
386
410
|
format: 'sse',
|
|
387
411
|
},
|
|
388
412
|
});
|