@yeaft/webchat-agent 0.1.703 → 0.1.704
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 +21 -0
- package/unify/llm/anthropic.js +9 -13
- package/unify/llm/openai-responses.js +69 -8
package/package.json
CHANGED
package/unify/llm/adapter.js
CHANGED
|
@@ -135,6 +135,27 @@ export function redactRawRequest(req) {
|
|
|
135
135
|
return { url: req.url, method: req.method, headers, body: req.body };
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Snapshot a Fetch Response's headers into a plain object for the debug
|
|
140
|
+
* panel. Defensive against polyfilled / mocked Response shapes that don't
|
|
141
|
+
* implement `Headers#entries()` — falls back to `{}` rather than throwing.
|
|
142
|
+
*
|
|
143
|
+
* NOTE: multi-valued headers (e.g. `Set-Cookie`) collapse to the last value
|
|
144
|
+
* because `Object.fromEntries` can't represent duplicates. For LLM debug
|
|
145
|
+
* traffic this is fine; if a future use case needs multi-valued capture,
|
|
146
|
+
* switch the return to an array of [k, v] pairs.
|
|
147
|
+
*
|
|
148
|
+
* @param {Response | { headers?: { entries?: () => Iterable<[string, string]> } }} response
|
|
149
|
+
* @returns {Record<string, string>}
|
|
150
|
+
*/
|
|
151
|
+
export function safeHeaders(response) {
|
|
152
|
+
const h = response && response.headers;
|
|
153
|
+
if (h && typeof h.entries === 'function') {
|
|
154
|
+
return Object.fromEntries(h.entries());
|
|
155
|
+
}
|
|
156
|
+
return {};
|
|
157
|
+
}
|
|
158
|
+
|
|
138
159
|
// ─── Base Class ────────────────────────────────────────────────
|
|
139
160
|
|
|
140
161
|
/**
|
package/unify/llm/anthropic.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
LLMServerError,
|
|
15
15
|
LLMAbortError,
|
|
16
16
|
redactRawRequest,
|
|
17
|
+
safeHeaders,
|
|
17
18
|
} from './adapter.js';
|
|
18
19
|
import {
|
|
19
20
|
normalizeEffort,
|
|
@@ -202,9 +203,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
202
203
|
rawRequest,
|
|
203
204
|
rawResponse: {
|
|
204
205
|
status: response.status,
|
|
205
|
-
headers: response
|
|
206
|
-
? Object.fromEntries(response.headers.entries())
|
|
207
|
-
: {},
|
|
206
|
+
headers: safeHeaders(response),
|
|
208
207
|
body: errorBody,
|
|
209
208
|
},
|
|
210
209
|
});
|
|
@@ -221,14 +220,11 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
221
220
|
let currentToolName = null;
|
|
222
221
|
let currentToolInput = '';
|
|
223
222
|
// Accumulate raw SSE body verbatim for the debug panel. No truncation:
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const responseHeaders = response.headers && typeof response.headers.entries === 'function'
|
|
230
|
-
? Object.fromEntries(response.headers.entries())
|
|
231
|
-
: {};
|
|
223
|
+
// see `redactRawRequest` in adapter.js for the verbatim-design rationale.
|
|
224
|
+
// Push-then-join keeps allocation bounded for multi-MiB payloads (avoids
|
|
225
|
+
// O(n²) string concat).
|
|
226
|
+
const rawSseBodyChunks = [];
|
|
227
|
+
const responseHeaders = safeHeaders(response);
|
|
232
228
|
const responseStatus = response.status;
|
|
233
229
|
|
|
234
230
|
try {
|
|
@@ -238,7 +234,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
238
234
|
|
|
239
235
|
const chunkText = decoder.decode(value, { stream: true });
|
|
240
236
|
buffer += chunkText;
|
|
241
|
-
|
|
237
|
+
rawSseBodyChunks.push(chunkText);
|
|
242
238
|
const lines = buffer.split('\n');
|
|
243
239
|
buffer = lines.pop() || ''; // Keep incomplete line
|
|
244
240
|
|
|
@@ -337,7 +333,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
337
333
|
rawResponse: {
|
|
338
334
|
status: responseStatus,
|
|
339
335
|
headers: responseHeaders,
|
|
340
|
-
body:
|
|
336
|
+
body: rawSseBodyChunks.join(''),
|
|
341
337
|
format: 'sse',
|
|
342
338
|
},
|
|
343
339
|
});
|
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
LLMContextError,
|
|
33
33
|
LLMServerError,
|
|
34
34
|
LLMAbortError,
|
|
35
|
+
redactRawRequest,
|
|
36
|
+
safeHeaders,
|
|
35
37
|
} from './adapter.js';
|
|
36
38
|
import {
|
|
37
39
|
normalizeEffort,
|
|
@@ -225,9 +227,16 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
225
227
|
// ─── Streaming ──────────────────────────────────────────
|
|
226
228
|
|
|
227
229
|
/**
|
|
228
|
-
* @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'low'|'medium'|'high'|'max', extraBody?: object, signal?: AbortSignal }} params
|
|
230
|
+
* @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'low'|'medium'|'high'|'max', extraBody?: object, signal?: AbortSignal, onRawExchange?: ({rawRequest, rawResponse}) => void }} params
|
|
231
|
+
*
|
|
232
|
+
* NOTE on `extraBody`: any keys you spread here are merged verbatim into
|
|
233
|
+
* the wire body and — because the verbatim debug feature is intentionally
|
|
234
|
+
* non-truncating — will surface in the debug panel via `rawRequest.body`.
|
|
235
|
+
* Do NOT put secrets in `extraBody`. Only `Authorization` / `x-api-key` /
|
|
236
|
+
* `api-key` headers are auto-redacted (see `redactRawRequest` in
|
|
237
|
+
* `adapter.js`); request-body fields are caller-controlled.
|
|
229
238
|
*/
|
|
230
|
-
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, extraBody, signal }) {
|
|
239
|
+
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, extraBody, signal, onRawExchange }) {
|
|
231
240
|
if (signal?.aborted) throw new LLMAbortError();
|
|
232
241
|
|
|
233
242
|
const body = {
|
|
@@ -255,14 +264,21 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
255
264
|
|
|
256
265
|
if (extraBody) Object.assign(body, extraBody);
|
|
257
266
|
|
|
267
|
+
const url = `${this.#baseUrl}/responses`;
|
|
268
|
+
const headers = {
|
|
269
|
+
'Content-Type': 'application/json',
|
|
270
|
+
'Authorization': `Bearer ${this.#apiKey}`,
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// Expose the raw request (auth-redacted) for the debug panel. See
|
|
274
|
+
// `redactRawRequest` in adapter.js for the verbatim-design rationale.
|
|
275
|
+
const rawRequest = redactRawRequest({ url, method: 'POST', headers, body });
|
|
276
|
+
|
|
258
277
|
let response;
|
|
259
278
|
try {
|
|
260
|
-
response = await fetch(
|
|
279
|
+
response = await fetch(url, {
|
|
261
280
|
method: 'POST',
|
|
262
|
-
headers
|
|
263
|
-
'Content-Type': 'application/json',
|
|
264
|
-
'Authorization': `Bearer ${this.#apiKey}`,
|
|
265
|
-
},
|
|
281
|
+
headers,
|
|
266
282
|
body: JSON.stringify(body),
|
|
267
283
|
signal,
|
|
268
284
|
});
|
|
@@ -273,6 +289,19 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
273
289
|
|
|
274
290
|
if (!response.ok) {
|
|
275
291
|
const errorBody = await response.text();
|
|
292
|
+
// Capture error response too, then throw. Parity with anthropic.js.
|
|
293
|
+
if (onRawExchange) {
|
|
294
|
+
try {
|
|
295
|
+
onRawExchange({
|
|
296
|
+
rawRequest,
|
|
297
|
+
rawResponse: {
|
|
298
|
+
status: response.status,
|
|
299
|
+
headers: safeHeaders(response),
|
|
300
|
+
body: errorBody,
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
} catch { /* ignore */ }
|
|
304
|
+
}
|
|
276
305
|
throw this.#classifyError(response.status, errorBody);
|
|
277
306
|
}
|
|
278
307
|
|
|
@@ -287,11 +316,21 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
287
316
|
const emittedToolCallIds = new Set();
|
|
288
317
|
let sawToolCall = false;
|
|
289
318
|
|
|
319
|
+
// Accumulate raw SSE body verbatim for the debug panel. No truncation:
|
|
320
|
+
// see `redactRawRequest` in adapter.js for the verbatim-design rationale.
|
|
321
|
+
// Push-then-join keeps allocation bounded for multi-MiB payloads (avoids
|
|
322
|
+
// O(n²) string concat).
|
|
323
|
+
const rawSseBodyChunks = [];
|
|
324
|
+
const responseHeaders = safeHeaders(response);
|
|
325
|
+
const responseStatus = response.status;
|
|
326
|
+
|
|
290
327
|
try {
|
|
291
328
|
while (true) {
|
|
292
329
|
const { done, value } = await reader.read();
|
|
293
330
|
if (done) break;
|
|
294
|
-
|
|
331
|
+
const chunkText = decoder.decode(value, { stream: true });
|
|
332
|
+
buffer += chunkText;
|
|
333
|
+
rawSseBodyChunks.push(chunkText);
|
|
295
334
|
|
|
296
335
|
// SSE events are separated by blank lines; split on \n
|
|
297
336
|
const lines = buffer.split('\n');
|
|
@@ -409,11 +448,33 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
409
448
|
throw err;
|
|
410
449
|
} finally {
|
|
411
450
|
try { reader.releaseLock(); } catch { /* noop */ }
|
|
451
|
+
// Emit raw exchange after stream completes (or errors). Body is the
|
|
452
|
+
// verbatim SSE — never truncated. Parity with anthropic.js.
|
|
453
|
+
if (onRawExchange) {
|
|
454
|
+
try {
|
|
455
|
+
onRawExchange({
|
|
456
|
+
rawRequest,
|
|
457
|
+
rawResponse: {
|
|
458
|
+
status: responseStatus,
|
|
459
|
+
headers: responseHeaders,
|
|
460
|
+
body: rawSseBodyChunks.join(''),
|
|
461
|
+
format: 'sse',
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
} catch { /* ignore */ }
|
|
465
|
+
}
|
|
412
466
|
}
|
|
413
467
|
}
|
|
414
468
|
|
|
415
469
|
// ─── Non-streaming call() ───────────────────────────────
|
|
416
470
|
|
|
471
|
+
/**
|
|
472
|
+
* Side-query (consolidate / dream / recall / light) entry point. Does
|
|
473
|
+
* NOT accept `onRawExchange` — these calls intentionally don't surface
|
|
474
|
+
* in the user-facing debug panel. If a future product change wants to
|
|
475
|
+
* expose them, mirror the stream() instrumentation. Parity with
|
|
476
|
+
* anthropic.js's `call()`.
|
|
477
|
+
*/
|
|
417
478
|
async call({ model, system, messages, maxTokens = 4096, effort, extraBody, signal }) {
|
|
418
479
|
if (signal?.aborted) throw new LLMAbortError();
|
|
419
480
|
|