@sigil0/looking-glass 0.2.5 → 0.3.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.
- package/README.md +25 -7
- package/dist/cli.js +12 -4
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +21 -4
- package/dist/config.js.map +1 -1
- package/dist/engine/engine.d.ts.map +1 -1
- package/dist/engine/engine.js +3 -0
- package/dist/engine/engine.js.map +1 -1
- package/dist/errors.d.ts +33 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +316 -0
- package/dist/errors.js.map +1 -0
- package/dist/model/codex-lb.d.ts +34 -41
- package/dist/model/codex-lb.d.ts.map +1 -1
- package/dist/model/codex-lb.js +603 -81
- package/dist/model/codex-lb.js.map +1 -1
- package/dist/storage/database.d.ts.map +1 -1
- package/dist/storage/database.js +55 -1
- package/dist/storage/database.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/model/codex-lb.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { redactSensitiveText } from "../security.js";
|
|
1
|
+
import { providerError, providerHttpError } from "../errors.js";
|
|
3
2
|
// Current llama.cpp grammar generation can emit invalid GBNF for nested string
|
|
4
3
|
// maxLength constraints at or above 2000. Keep the original schemas for local
|
|
5
4
|
// validation and only relax the cloned schema sent to LM Studio.
|
|
@@ -29,6 +28,8 @@ function lmStudioTools(tools) {
|
|
|
29
28
|
});
|
|
30
29
|
}
|
|
31
30
|
export function buildResponseParams(provider, request) {
|
|
31
|
+
if (provider === "openrouter")
|
|
32
|
+
return buildOpenRouterParams(request);
|
|
32
33
|
const common = {
|
|
33
34
|
model: request.model,
|
|
34
35
|
instructions: request.instructions,
|
|
@@ -36,7 +37,7 @@ export function buildResponseParams(provider, request) {
|
|
|
36
37
|
? request.input.filter((item) => item.type !== "reasoning")
|
|
37
38
|
: request.input,
|
|
38
39
|
tools: provider === "lm-studio" ? lmStudioTools(request.tools) : request.tools,
|
|
39
|
-
parallel_tool_calls: true,
|
|
40
|
+
...(request.supportsParallelToolCalls ? { parallel_tool_calls: true } : {}),
|
|
40
41
|
...(request.supportsReasoning ? {
|
|
41
42
|
reasoning: {
|
|
42
43
|
// codex-lb advertises "ultra" before the public SDK type includes it.
|
|
@@ -66,14 +67,14 @@ function detailFrom(value) {
|
|
|
66
67
|
const outer = value;
|
|
67
68
|
return outer.error && typeof outer.error === "object" ? outer.error : outer;
|
|
68
69
|
}
|
|
69
|
-
async function* responseEvents(response) {
|
|
70
|
+
async function* responseEvents(response, context) {
|
|
70
71
|
if (!response.body)
|
|
71
|
-
throw
|
|
72
|
-
const
|
|
72
|
+
throw providerError({ code: "malformed_response", message: "response had no body" }, { ...(context ?? { provider: "provider", operation: "stream" }), protocol: true });
|
|
73
|
+
const streamReader = response.body.getReader();
|
|
73
74
|
const decoder = new TextDecoder();
|
|
74
75
|
let buffer = "";
|
|
75
76
|
let data = [];
|
|
76
|
-
const
|
|
77
|
+
const parseResponseEvent = () => {
|
|
77
78
|
const payload = data.join("\n");
|
|
78
79
|
data = [];
|
|
79
80
|
if (!payload || payload === "[DONE]")
|
|
@@ -86,20 +87,20 @@ async function* responseEvents(response) {
|
|
|
86
87
|
return event;
|
|
87
88
|
}
|
|
88
89
|
catch (error) {
|
|
89
|
-
throw Object.assign(new Error(`Malformed response stream event: ${error instanceof Error ? error.message : String(error)}`), {
|
|
90
|
+
throw providerError(Object.assign(new Error(`Malformed response stream event: ${error instanceof Error ? error.message : String(error)}`), {
|
|
90
91
|
code: "malformed_response_event",
|
|
91
|
-
});
|
|
92
|
+
}), { ...(context ?? { provider: "provider", operation: "stream" }), protocol: true });
|
|
92
93
|
}
|
|
93
94
|
};
|
|
94
95
|
while (true) {
|
|
95
|
-
const chunk = await
|
|
96
|
+
const chunk = context ? await readStreamChunk(streamReader, context) : await streamReader.read();
|
|
96
97
|
buffer += decoder.decode(chunk.value ?? new Uint8Array(), { stream: !chunk.done });
|
|
97
98
|
let newline = buffer.indexOf("\n");
|
|
98
99
|
while (newline >= 0) {
|
|
99
100
|
const line = buffer.slice(0, newline).replace(/\r$/, "");
|
|
100
101
|
buffer = buffer.slice(newline + 1);
|
|
101
102
|
if (line === "") {
|
|
102
|
-
const event =
|
|
103
|
+
const event = parseResponseEvent();
|
|
103
104
|
if (event)
|
|
104
105
|
yield event;
|
|
105
106
|
}
|
|
@@ -113,7 +114,7 @@ async function* responseEvents(response) {
|
|
|
113
114
|
}
|
|
114
115
|
if (buffer.startsWith("data:"))
|
|
115
116
|
data.push(buffer.slice(5).trimStart());
|
|
116
|
-
const event =
|
|
117
|
+
const event = parseResponseEvent();
|
|
117
118
|
if (event)
|
|
118
119
|
yield event;
|
|
119
120
|
}
|
|
@@ -203,6 +204,42 @@ export function lmStudioModelInfo(raw) {
|
|
|
203
204
|
priority: raw.loaded_instances?.length ? 0 : 1_000,
|
|
204
205
|
};
|
|
205
206
|
}
|
|
207
|
+
function numericPrice(value) {
|
|
208
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
209
|
+
return value;
|
|
210
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
211
|
+
return null;
|
|
212
|
+
const parsed = Number(value);
|
|
213
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
214
|
+
}
|
|
215
|
+
export function openRouterModelInfo(raw) {
|
|
216
|
+
const supported = new Set(raw.supported_parameters ?? []);
|
|
217
|
+
const architecture = raw.architecture ?? {};
|
|
218
|
+
const pricing = raw.pricing ?? {};
|
|
219
|
+
const isFree = raw.id.toLowerCase().endsWith(":free")
|
|
220
|
+
|| (numericPrice(pricing.prompt) === 0 && numericPrice(pricing.completion) === 0);
|
|
221
|
+
const supportsReasoning = supported.has("reasoning");
|
|
222
|
+
const supportsImages = (architecture.input_modalities ?? []).some((value) => /image/i.test(value));
|
|
223
|
+
const supportsTools = supported.has("tools") || supported.has("tool_choice")
|
|
224
|
+
|| supported.has("parallel_tool_calls") || supported.has("function_calling");
|
|
225
|
+
return {
|
|
226
|
+
id: raw.id,
|
|
227
|
+
name: raw.name ?? raw.id,
|
|
228
|
+
description: raw.description ?? "",
|
|
229
|
+
contextWindow: raw.context_length ?? raw.top_provider?.context_length ?? 128_000,
|
|
230
|
+
maxOutputTokens: raw.max_completion_tokens ?? raw.top_provider?.max_completion_tokens
|
|
231
|
+
?? raw.max_output_tokens ?? null,
|
|
232
|
+
reasoningEfforts: supportsReasoning ? ["low", "medium", "high"] : ["none"],
|
|
233
|
+
defaultReasoningEffort: supportsReasoning ? "medium" : "none",
|
|
234
|
+
defaultVerbosity: "low",
|
|
235
|
+
supportsReasoning,
|
|
236
|
+
supportsImages,
|
|
237
|
+
supportsParallelToolCalls: supportsTools && supported.has("parallel_tool_calls"),
|
|
238
|
+
supportsFast: false,
|
|
239
|
+
priority: isFree ? 0 : 1_000,
|
|
240
|
+
isFree,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
206
243
|
export function responseText(response) {
|
|
207
244
|
if (response.output_text)
|
|
208
245
|
return response.output_text;
|
|
@@ -257,40 +294,285 @@ function lmStudioModelsURL(baseURL) {
|
|
|
257
294
|
url.pathname = `${url.pathname.replace(/\/?v1\/?$/, "")}/api/v1/models`.replace(/\/+/g, "/");
|
|
258
295
|
return url.toString();
|
|
259
296
|
}
|
|
297
|
+
function chatContent(value) {
|
|
298
|
+
if (typeof value === "string")
|
|
299
|
+
return value;
|
|
300
|
+
if (!Array.isArray(value))
|
|
301
|
+
return "";
|
|
302
|
+
const parts = value.flatMap((part) => {
|
|
303
|
+
if (!part || typeof part !== "object")
|
|
304
|
+
return [];
|
|
305
|
+
const item = part;
|
|
306
|
+
if ((item.type === "input_text" || item.type === "output_text" || item.type === "text")
|
|
307
|
+
&& typeof item.text === "string")
|
|
308
|
+
return [{ type: "text", text: item.text }];
|
|
309
|
+
if (item.type === "input_image" && typeof item.image_url === "string") {
|
|
310
|
+
return [{ type: "image_url", image_url: { url: item.image_url } }];
|
|
311
|
+
}
|
|
312
|
+
return [];
|
|
313
|
+
});
|
|
314
|
+
return parts.length === 1 && typeof parts[0] === "string" ? parts[0] : parts;
|
|
315
|
+
}
|
|
316
|
+
export function openRouterMessages(instructions, input) {
|
|
317
|
+
const messages = [];
|
|
318
|
+
if (instructions.trim())
|
|
319
|
+
messages.push({ role: "system", content: instructions });
|
|
320
|
+
for (const raw of input) {
|
|
321
|
+
const item = raw;
|
|
322
|
+
if (item.type === "reasoning" || item.type === "compaction")
|
|
323
|
+
continue;
|
|
324
|
+
if (item.type === "function_call") {
|
|
325
|
+
const previous = messages.at(-1);
|
|
326
|
+
const call = {
|
|
327
|
+
id: typeof item.call_id === "string" ? item.call_id : String(item.id ?? "call_unknown"),
|
|
328
|
+
type: "function",
|
|
329
|
+
function: {
|
|
330
|
+
name: typeof item.name === "string" ? item.name : "tool",
|
|
331
|
+
arguments: typeof item.arguments === "string" ? item.arguments : JSON.stringify(item.arguments ?? {}),
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
if (previous?.role === "assistant") {
|
|
335
|
+
if (Array.isArray(previous.tool_calls))
|
|
336
|
+
previous.tool_calls.push(call);
|
|
337
|
+
else
|
|
338
|
+
previous.tool_calls = [call];
|
|
339
|
+
}
|
|
340
|
+
else
|
|
341
|
+
messages.push({ role: "assistant", content: null, tool_calls: [call] });
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (item.type === "function_call_output") {
|
|
345
|
+
messages.push({
|
|
346
|
+
role: "tool",
|
|
347
|
+
tool_call_id: typeof item.call_id === "string" ? item.call_id : String(item.id ?? "call_unknown"),
|
|
348
|
+
content: typeof item.output === "string" ? item.output : JSON.stringify(item.output ?? ""),
|
|
349
|
+
});
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const role = item.role === "developer" ? "system"
|
|
353
|
+
: item.role === "assistant" ? "assistant" : item.role === "system" ? "system" : "user";
|
|
354
|
+
messages.push({ role, content: chatContent(item.content) });
|
|
355
|
+
}
|
|
356
|
+
return messages;
|
|
357
|
+
}
|
|
358
|
+
export function buildOpenRouterParams(request) {
|
|
359
|
+
const tools = request.tools.map((tool) => ({
|
|
360
|
+
type: "function",
|
|
361
|
+
function: {
|
|
362
|
+
name: tool.name,
|
|
363
|
+
description: tool.description,
|
|
364
|
+
parameters: tool.parameters,
|
|
365
|
+
...(tool.strict === undefined ? {} : { strict: tool.strict }),
|
|
366
|
+
},
|
|
367
|
+
}));
|
|
368
|
+
return {
|
|
369
|
+
model: request.model,
|
|
370
|
+
messages: openRouterMessages(request.instructions, request.input),
|
|
371
|
+
...(tools.length > 0
|
|
372
|
+
? { tools, ...(request.supportsParallelToolCalls ? { parallel_tool_calls: true } : {}) }
|
|
373
|
+
: {}),
|
|
374
|
+
...(request.supportsReasoning && request.reasoningEffort !== "none"
|
|
375
|
+
? { reasoning: { effort: request.reasoningEffort } } : {}),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
const MAX_HTTP_ERROR_BYTES = 64 * 1024;
|
|
379
|
+
async function boundedText(response, limit = MAX_HTTP_ERROR_BYTES) {
|
|
380
|
+
if (!response.body)
|
|
381
|
+
return "";
|
|
382
|
+
const reader = response.body.getReader();
|
|
383
|
+
const chunks = [];
|
|
384
|
+
let total = 0;
|
|
385
|
+
while (total < limit) {
|
|
386
|
+
const next = await reader.read();
|
|
387
|
+
if (next.done)
|
|
388
|
+
break;
|
|
389
|
+
const chunk = next.value ?? new Uint8Array();
|
|
390
|
+
const take = Math.min(chunk.byteLength, limit - total);
|
|
391
|
+
if (take > 0)
|
|
392
|
+
chunks.push(chunk.slice(0, take));
|
|
393
|
+
total += take;
|
|
394
|
+
if (take < chunk.byteLength) {
|
|
395
|
+
await reader.cancel();
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return new TextDecoder().decode(Buffer.concat(chunks));
|
|
400
|
+
}
|
|
401
|
+
async function boundedResponseText(response, context) {
|
|
402
|
+
try {
|
|
403
|
+
return await boundedText(response);
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
throw providerError(error, context);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function parseJsonBody(text) {
|
|
410
|
+
if (!text.trim())
|
|
411
|
+
return "";
|
|
412
|
+
try {
|
|
413
|
+
return JSON.parse(text);
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
return text;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function normalizedUsage(value) {
|
|
420
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
421
|
+
return undefined;
|
|
422
|
+
const usage = value;
|
|
423
|
+
const numberValue = (...keys) => {
|
|
424
|
+
for (const key of keys) {
|
|
425
|
+
const candidate = usage[key];
|
|
426
|
+
if (typeof candidate === "number" && Number.isFinite(candidate) && candidate >= 0)
|
|
427
|
+
return candidate;
|
|
428
|
+
}
|
|
429
|
+
return 0;
|
|
430
|
+
};
|
|
431
|
+
const inputTokens = numberValue("input_tokens", "prompt_tokens");
|
|
432
|
+
const outputTokens = numberValue("output_tokens", "completion_tokens");
|
|
433
|
+
return {
|
|
434
|
+
input_tokens: inputTokens,
|
|
435
|
+
output_tokens: outputTokens,
|
|
436
|
+
total_tokens: numberValue("total_tokens") || inputTokens + outputTokens,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function requestContext(provider, operation, apiKey, signal, timeoutSignal) {
|
|
440
|
+
return {
|
|
441
|
+
provider,
|
|
442
|
+
operation,
|
|
443
|
+
configuredSecrets: [apiKey],
|
|
444
|
+
...(signal ? { callerSignal: signal } : {}),
|
|
445
|
+
...(timeoutSignal ? { timeoutSignal } : {}),
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
async function readJsonResponse(response, context) {
|
|
449
|
+
let text;
|
|
450
|
+
try {
|
|
451
|
+
text = response.ok ? await response.text() : await boundedText(response);
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
throw providerError(error, context);
|
|
455
|
+
}
|
|
456
|
+
if (!response.ok)
|
|
457
|
+
throw providerHttpError(response.status, parseJsonBody(text), context);
|
|
458
|
+
try {
|
|
459
|
+
return JSON.parse(text);
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
throw providerError(Object.assign(new Error("response was not valid JSON"), { cause: error }), {
|
|
463
|
+
...context,
|
|
464
|
+
protocol: true,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function objectPayload(payload, context, message) {
|
|
469
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
470
|
+
throw providerError({ code: "malformed_response", message }, { ...context, protocol: true });
|
|
471
|
+
}
|
|
472
|
+
return payload;
|
|
473
|
+
}
|
|
474
|
+
async function readStreamChunk(reader, context) {
|
|
475
|
+
try {
|
|
476
|
+
return await reader.read();
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
throw providerError(error, context);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async function* openRouterEvents(response, context) {
|
|
483
|
+
if (!response.body)
|
|
484
|
+
throw providerError({ code: "malformed_response", message: "response had no body" }, { ...context, protocol: true });
|
|
485
|
+
const streamReader = response.body.getReader();
|
|
486
|
+
const decoder = new TextDecoder();
|
|
487
|
+
let buffer = "";
|
|
488
|
+
let data = [];
|
|
489
|
+
let ended = false;
|
|
490
|
+
const emit = () => {
|
|
491
|
+
const payload = data.join("\n").trim();
|
|
492
|
+
data = [];
|
|
493
|
+
if (!payload)
|
|
494
|
+
return null;
|
|
495
|
+
if (payload === "[DONE]")
|
|
496
|
+
return { done: true };
|
|
497
|
+
try {
|
|
498
|
+
return JSON.parse(payload);
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
throw providerError(Object.assign(new Error("malformed SSE event JSON"), {
|
|
502
|
+
cause: error,
|
|
503
|
+
code: "malformed_response_event",
|
|
504
|
+
}), { ...context, protocol: true });
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
try {
|
|
508
|
+
while (true) {
|
|
509
|
+
const chunk = await readStreamChunk(streamReader, context);
|
|
510
|
+
buffer += decoder.decode(chunk.value ?? new Uint8Array(), { stream: !chunk.done });
|
|
511
|
+
let newline = buffer.indexOf("\n");
|
|
512
|
+
while (newline >= 0) {
|
|
513
|
+
const line = buffer.slice(0, newline).replace(/\r$/, "");
|
|
514
|
+
buffer = buffer.slice(newline + 1);
|
|
515
|
+
if (line === "") {
|
|
516
|
+
const event = emit();
|
|
517
|
+
if (event) {
|
|
518
|
+
if (typeof event === "object" && event !== null && "done" in event)
|
|
519
|
+
ended = true;
|
|
520
|
+
yield event;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
else if (line.startsWith("data:"))
|
|
524
|
+
data.push(line.slice(5).trimStart());
|
|
525
|
+
newline = buffer.indexOf("\n");
|
|
526
|
+
}
|
|
527
|
+
if (chunk.done)
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
if (buffer.startsWith("data:"))
|
|
531
|
+
data.push(buffer.slice(5).trimStart());
|
|
532
|
+
const event = emit();
|
|
533
|
+
if (event) {
|
|
534
|
+
if (typeof event === "object" && event !== null && "done" in event)
|
|
535
|
+
ended = true;
|
|
536
|
+
yield event;
|
|
537
|
+
}
|
|
538
|
+
if (!ended)
|
|
539
|
+
throw providerError({ code: "stream_incomplete", message: "response stream ended without [DONE]" }, context);
|
|
540
|
+
}
|
|
541
|
+
finally {
|
|
542
|
+
try {
|
|
543
|
+
await streamReader.cancel();
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
// The stream may already be closed by the fetch implementation.
|
|
547
|
+
}
|
|
548
|
+
streamReader.releaseLock();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
260
551
|
export class CodexLbClient {
|
|
261
552
|
config;
|
|
262
|
-
client;
|
|
263
553
|
apiKey;
|
|
264
554
|
constructor(config) {
|
|
265
555
|
this.config = config;
|
|
266
556
|
this.apiKey = process.env[config.gateway.apiKeyEnv] || "local-looking-glass";
|
|
267
|
-
this.client = new OpenAI({
|
|
268
|
-
apiKey: this.apiKey,
|
|
269
|
-
baseURL: config.gateway.baseURL,
|
|
270
|
-
maxRetries: 0,
|
|
271
|
-
timeout: config.gateway.timeoutMs,
|
|
272
|
-
});
|
|
273
557
|
}
|
|
274
558
|
supportsResponseContinuity() {
|
|
275
559
|
return this.config.gateway.provider === "codex-lb";
|
|
276
560
|
}
|
|
277
|
-
safeErrorMessage(message) {
|
|
278
|
-
return redactSensitiveText(message, [this.apiKey]);
|
|
279
|
-
}
|
|
280
561
|
async models(signal) {
|
|
562
|
+
const provider = this.config.gateway.provider;
|
|
563
|
+
const timeout = AbortSignal.timeout(this.config.gateway.timeoutMs);
|
|
564
|
+
const requestSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
565
|
+
const context = requestContext(provider, "models", this.apiKey, signal, timeout);
|
|
281
566
|
if (this.config.gateway.provider === "lm-studio") {
|
|
282
567
|
try {
|
|
283
|
-
const timeout = AbortSignal.timeout(this.config.gateway.timeoutMs);
|
|
284
|
-
const requestSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
285
568
|
const response = await fetch(lmStudioModelsURL(this.config.gateway.baseURL), {
|
|
286
569
|
headers: { authorization: `Bearer ${this.apiKey}` },
|
|
287
570
|
signal: requestSignal,
|
|
288
571
|
});
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
.filter((model) => model.type === "llm")
|
|
572
|
+
const nativeContext = { ...context, operation: "models (native catalog)" };
|
|
573
|
+
const payload = objectPayload(await readJsonResponse(response, nativeContext), nativeContext, "model catalog returned an invalid payload");
|
|
574
|
+
const models = (Array.isArray(payload.models) ? payload.models : [])
|
|
575
|
+
.filter((model) => model && typeof model === "object" && model.type === "llm" && typeof model.key === "string")
|
|
294
576
|
.map(lmStudioModelInfo)
|
|
295
577
|
.sort((left, right) => left.priority - right.priority || left.name.localeCompare(right.name));
|
|
296
578
|
if (models.length > 0)
|
|
@@ -302,47 +584,196 @@ export class CodexLbClient {
|
|
|
302
584
|
// Older LM Studio versions and restricted tokens may only expose /v1/models.
|
|
303
585
|
}
|
|
304
586
|
}
|
|
305
|
-
|
|
306
|
-
|
|
587
|
+
let response;
|
|
588
|
+
try {
|
|
589
|
+
response = await fetch(`${this.config.gateway.baseURL.replace(/\/$/, "")}/models`, {
|
|
590
|
+
headers: { authorization: `Bearer ${this.apiKey}` },
|
|
591
|
+
signal: requestSignal,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
throw providerError(error, context);
|
|
596
|
+
}
|
|
597
|
+
const payload = objectPayload(await readJsonResponse(response, context), context, "model catalog returned an invalid payload");
|
|
598
|
+
if (payload.data !== undefined && !Array.isArray(payload.data)) {
|
|
599
|
+
throw providerError({ code: "malformed_response", message: "model catalog data was not an array" }, { ...context, protocol: true });
|
|
600
|
+
}
|
|
601
|
+
const models = (payload.data ?? []).filter((model) => model && typeof model === "object" && typeof model.id === "string");
|
|
602
|
+
if (provider === "openrouter") {
|
|
603
|
+
return models
|
|
604
|
+
.map((model) => openRouterModelInfo(model))
|
|
605
|
+
.sort((left, right) => left.priority - right.priority || left.name.localeCompare(right.name));
|
|
606
|
+
}
|
|
607
|
+
return models
|
|
307
608
|
.filter((model) => model.metadata?.supported_in_api !== false)
|
|
308
609
|
.map(modelInfo)
|
|
309
610
|
.sort((left, right) => left.priority - right.priority || left.name.localeCompare(right.name));
|
|
310
611
|
}
|
|
311
|
-
async
|
|
312
|
-
const params = buildResponseParams(this.config.gateway.provider, request);
|
|
612
|
+
async openRouterStream(request, callbacks) {
|
|
313
613
|
const timeout = AbortSignal.timeout(this.config.gateway.timeoutMs);
|
|
314
614
|
const signal = request.signal ? AbortSignal.any([request.signal, timeout]) : timeout;
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
615
|
+
const context = requestContext("openrouter", "stream", this.apiKey, request.signal, timeout);
|
|
616
|
+
let http;
|
|
617
|
+
try {
|
|
618
|
+
http = await fetch(`${this.config.gateway.baseURL.replace(/\/$/, "")}/chat/completions`, {
|
|
619
|
+
method: "POST",
|
|
620
|
+
headers: {
|
|
621
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
622
|
+
"content-type": "application/json",
|
|
623
|
+
accept: "text/event-stream",
|
|
624
|
+
},
|
|
625
|
+
body: JSON.stringify({ ...buildOpenRouterParams(request), stream: true, stream_options: { include_usage: true } }),
|
|
626
|
+
signal,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
throw providerError(error, context);
|
|
631
|
+
}
|
|
325
632
|
if (!http.ok) {
|
|
326
|
-
const text = await http
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
633
|
+
const text = await boundedResponseText(http, context);
|
|
634
|
+
throw providerHttpError(http.status, parseJsonBody(text), context);
|
|
635
|
+
}
|
|
636
|
+
const outputText = [];
|
|
637
|
+
const reasoningText = [];
|
|
638
|
+
const toolCalls = new Map();
|
|
639
|
+
let id = "";
|
|
640
|
+
let model = request.model;
|
|
641
|
+
let usage;
|
|
642
|
+
callbacks.onEvent?.({ type: "response.created", response: { id: "", status: "in_progress", model, output: [] } });
|
|
643
|
+
for await (const raw of openRouterEvents(http, context)) {
|
|
644
|
+
if (!raw || typeof raw !== "object")
|
|
645
|
+
continue;
|
|
646
|
+
if (raw.done)
|
|
647
|
+
break;
|
|
648
|
+
const event = raw;
|
|
649
|
+
if (event.error !== undefined)
|
|
650
|
+
throw providerError(event.error, context);
|
|
651
|
+
if (typeof event.id === "string")
|
|
652
|
+
id ||= event.id;
|
|
653
|
+
if (typeof event.model === "string")
|
|
654
|
+
model = event.model;
|
|
655
|
+
if (event.usage && typeof event.usage === "object")
|
|
656
|
+
usage = event.usage;
|
|
657
|
+
const choices = Array.isArray(event.choices) ? event.choices : [];
|
|
658
|
+
for (const choice of choices) {
|
|
659
|
+
if (!choice || typeof choice !== "object")
|
|
660
|
+
continue;
|
|
661
|
+
const delta = choice.delta;
|
|
662
|
+
if (!delta || typeof delta !== "object")
|
|
663
|
+
continue;
|
|
664
|
+
const item = delta;
|
|
665
|
+
const text = typeof item.content === "string" ? item.content : "";
|
|
666
|
+
if (text) {
|
|
667
|
+
outputText.push(text);
|
|
668
|
+
callbacks.onTextDelta?.(text);
|
|
669
|
+
callbacks.onEvent?.({ type: "response.output_text.delta", delta: text });
|
|
670
|
+
}
|
|
671
|
+
const detailReasoning = Array.isArray(item.reasoning_details)
|
|
672
|
+
? item.reasoning_details.map((detail) => detail && typeof detail === "object"
|
|
673
|
+
&& typeof detail.text === "string"
|
|
674
|
+
? detail.text : "").join("")
|
|
675
|
+
: "";
|
|
676
|
+
const reasoning = typeof item.reasoning === "string" ? item.reasoning
|
|
677
|
+
: typeof item.reasoning_content === "string" ? item.reasoning_content : detailReasoning;
|
|
678
|
+
if (reasoning) {
|
|
679
|
+
reasoningText.push(reasoning);
|
|
680
|
+
callbacks.onReasoningDelta?.(reasoning);
|
|
681
|
+
callbacks.onEvent?.({ type: "response.reasoning_summary_text.delta", delta: reasoning });
|
|
682
|
+
}
|
|
683
|
+
if (Array.isArray(item.tool_calls)) {
|
|
684
|
+
for (const tool of item.tool_calls) {
|
|
685
|
+
if (!tool || typeof tool !== "object")
|
|
686
|
+
continue;
|
|
687
|
+
const call = tool;
|
|
688
|
+
const index = typeof call.index === "number" ? call.index : toolCalls.size;
|
|
689
|
+
const fn = call.function && typeof call.function === "object" ? call.function : {};
|
|
690
|
+
const current = toolCalls.get(index) ?? { id: "", name: "", arguments: "" };
|
|
691
|
+
if (typeof call.id === "string")
|
|
692
|
+
current.id = call.id;
|
|
693
|
+
if (typeof fn.name === "string")
|
|
694
|
+
current.name += fn.name;
|
|
695
|
+
if (typeof fn.arguments === "string")
|
|
696
|
+
current.arguments += fn.arguments;
|
|
697
|
+
toolCalls.set(index, current);
|
|
698
|
+
if (typeof fn.arguments === "string")
|
|
699
|
+
callbacks.onEvent?.({
|
|
700
|
+
type: "response.function_call_arguments.delta", delta: fn.arguments,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
333
704
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
705
|
+
}
|
|
706
|
+
id ||= `chatcmpl_${Date.now().toString(36)}`;
|
|
707
|
+
const output = [];
|
|
708
|
+
if (reasoningText.length > 0)
|
|
709
|
+
output.push({
|
|
710
|
+
id: `reasoning_${id}`, type: "reasoning", status: "completed", summary: [{ type: "summary_text", text: reasoningText.join("") }], content: [],
|
|
711
|
+
});
|
|
712
|
+
if (outputText.length > 0)
|
|
713
|
+
output.push({
|
|
714
|
+
id: `msg_${id}`, type: "message", role: "assistant", status: "completed",
|
|
715
|
+
content: [{ type: "output_text", text: outputText.join(""), annotations: [], logprobs: [] }],
|
|
716
|
+
});
|
|
717
|
+
const callOutputIndices = new Map();
|
|
718
|
+
for (const [index, call] of [...toolCalls.entries()].sort(([left], [right]) => left - right)) {
|
|
719
|
+
callOutputIndices.set(index, output.length);
|
|
720
|
+
output.push({ id: call.id || `call_${id}_${index}`, type: "function_call", call_id: call.id || `call_${id}_${index}`, name: call.name, arguments: call.arguments, status: "completed" });
|
|
721
|
+
}
|
|
722
|
+
if (output.length === 0)
|
|
723
|
+
throw providerError({ code: "malformed_response", message: "response contained no output" }, { ...context, protocol: true });
|
|
724
|
+
for (const [index, item] of output.entries())
|
|
725
|
+
callbacks.onEvent?.({ type: "response.output_item.done", output_index: index, item });
|
|
726
|
+
for (const [index, call] of [...toolCalls.entries()].sort(([left], [right]) => left - right)) {
|
|
727
|
+
callbacks.onEvent?.({
|
|
728
|
+
type: "response.function_call_arguments.done", output_index: callOutputIndices.get(index), arguments: call.arguments,
|
|
338
729
|
});
|
|
339
730
|
}
|
|
731
|
+
const response = {
|
|
732
|
+
id, object: "response", created: Math.floor(Date.now() / 1000), model, status: "completed", output,
|
|
733
|
+
output_text: outputText.join(""),
|
|
734
|
+
...(usage ? { usage: {
|
|
735
|
+
input_tokens: typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : 0,
|
|
736
|
+
output_tokens: typeof usage.completion_tokens === "number" ? usage.completion_tokens : 0,
|
|
737
|
+
total_tokens: typeof usage.total_tokens === "number" ? usage.total_tokens
|
|
738
|
+
: (typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : 0)
|
|
739
|
+
+ (typeof usage.completion_tokens === "number" ? usage.completion_tokens : 0),
|
|
740
|
+
} } : {}),
|
|
741
|
+
};
|
|
742
|
+
callbacks.onEvent?.({ type: "response.completed", response });
|
|
743
|
+
return response;
|
|
744
|
+
}
|
|
745
|
+
async stream(request, callbacks = {}) {
|
|
746
|
+
if (this.config.gateway.provider === "openrouter")
|
|
747
|
+
return this.openRouterStream(request, callbacks);
|
|
748
|
+
const params = buildResponseParams(this.config.gateway.provider, request);
|
|
749
|
+
const timeout = AbortSignal.timeout(this.config.gateway.timeoutMs);
|
|
750
|
+
const signal = request.signal ? AbortSignal.any([request.signal, timeout]) : timeout;
|
|
751
|
+
const context = requestContext(this.config.gateway.provider, "stream", this.apiKey, request.signal, timeout);
|
|
752
|
+
let http;
|
|
753
|
+
try {
|
|
754
|
+
http = await fetch(`${this.config.gateway.baseURL}/responses`, {
|
|
755
|
+
method: "POST",
|
|
756
|
+
headers: {
|
|
757
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
758
|
+
"content-type": "application/json",
|
|
759
|
+
accept: "text/event-stream",
|
|
760
|
+
},
|
|
761
|
+
body: JSON.stringify({ ...params, stream: true }),
|
|
762
|
+
signal,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
catch (error) {
|
|
766
|
+
throw providerError(error, context);
|
|
767
|
+
}
|
|
768
|
+
if (!http.ok) {
|
|
769
|
+
throw providerHttpError(http.status, parseJsonBody(await boundedResponseText(http, context)), context);
|
|
770
|
+
}
|
|
340
771
|
let created = {};
|
|
341
772
|
let terminal = null;
|
|
342
773
|
let streamedError = null;
|
|
343
774
|
let sawTerminal = false;
|
|
344
775
|
const output = new Map();
|
|
345
|
-
for await (const event of responseEvents(http)) {
|
|
776
|
+
for await (const event of responseEvents(http, context)) {
|
|
346
777
|
callbacks.onEvent?.(event);
|
|
347
778
|
if (event.type === "response.output_text.delta" && typeof event.delta === "string") {
|
|
348
779
|
callbacks.onTextDelta?.(event.delta);
|
|
@@ -370,10 +801,9 @@ export class CodexLbClient {
|
|
|
370
801
|
}
|
|
371
802
|
if (!sawTerminal) {
|
|
372
803
|
if (streamedError) {
|
|
373
|
-
|
|
374
|
-
throw Object.assign(new Error(this.safeErrorMessage(detail.message ?? "Response stream failed")), detail);
|
|
804
|
+
throw providerError(streamedError, context);
|
|
375
805
|
}
|
|
376
|
-
throw
|
|
806
|
+
throw providerError({ code: "stream_incomplete", message: "response stream ended without a terminal event" }, context);
|
|
377
807
|
}
|
|
378
808
|
const combined = { ...created, ...(terminal ?? {}) };
|
|
379
809
|
const terminalOutput = Array.isArray(combined.output) ? combined.output : [];
|
|
@@ -384,12 +814,15 @@ export class CodexLbClient {
|
|
|
384
814
|
? combined.status
|
|
385
815
|
: streamedError ? "failed" : "completed";
|
|
386
816
|
if (streamedError || status === "failed" || status === "incomplete") {
|
|
387
|
-
|
|
388
|
-
|
|
817
|
+
throw providerError(streamedError ?? combined.error ?? combined.incomplete_details ?? { message: `Response ${status}` }, {
|
|
818
|
+
...context,
|
|
819
|
+
responseStatus: status,
|
|
820
|
+
});
|
|
389
821
|
}
|
|
390
822
|
if (typeof combined.id !== "string" || !combined.id || canonicalOutput.length === 0) {
|
|
391
|
-
throw
|
|
392
|
-
|
|
823
|
+
throw providerError({ code: "malformed_response", message: "response stream returned an invalid completed response" }, {
|
|
824
|
+
...context,
|
|
825
|
+
protocol: true,
|
|
393
826
|
});
|
|
394
827
|
}
|
|
395
828
|
const canonical = {
|
|
@@ -403,10 +836,68 @@ export class CodexLbClient {
|
|
|
403
836
|
return this.config.gateway.provider === "lm-studio" ? redactLmStudioReasoning(canonical) : canonical;
|
|
404
837
|
}
|
|
405
838
|
async compact(request) {
|
|
839
|
+
if (this.config.gateway.provider === "openrouter") {
|
|
840
|
+
const transcript = compactTranscript(request.input);
|
|
841
|
+
if (!transcript)
|
|
842
|
+
throw providerError({ code: "malformed_response", message: "compaction received no semantic transcript" }, {
|
|
843
|
+
...requestContext("openrouter", "compact", this.apiKey, request.signal), protocol: true,
|
|
844
|
+
});
|
|
845
|
+
const timeout = AbortSignal.timeout(this.config.gateway.timeoutMs);
|
|
846
|
+
const signal = request.signal ? AbortSignal.any([request.signal, timeout]) : timeout;
|
|
847
|
+
const context = requestContext("openrouter", "compact", this.apiKey, request.signal, timeout);
|
|
848
|
+
let http;
|
|
849
|
+
try {
|
|
850
|
+
http = await fetch(`${this.config.gateway.baseURL.replace(/\/$/, "")}/chat/completions`, {
|
|
851
|
+
method: "POST",
|
|
852
|
+
headers: { authorization: `Bearer ${this.apiKey}`, "content-type": "application/json" },
|
|
853
|
+
body: JSON.stringify({
|
|
854
|
+
model: request.model,
|
|
855
|
+
messages: openRouterMessages([
|
|
856
|
+
request.instructions,
|
|
857
|
+
"Create a dense, durable checkpoint of the supplied conversation.",
|
|
858
|
+
"Preserve user requirements, decisions, relevant facts, file paths, code changes, tool outcomes, unresolved work, and safety constraints.",
|
|
859
|
+
"Do not continue the task, call tools, or add commentary. Return only the checkpoint text.",
|
|
860
|
+
].filter((part) => part.trim()).join(" "), [{ role: "user", content: [{ type: "input_text", text: `Conversation transcript:\n\n${transcript}` }] }]),
|
|
861
|
+
stream: false,
|
|
862
|
+
}),
|
|
863
|
+
signal,
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
catch (error) {
|
|
867
|
+
throw providerError(error, context);
|
|
868
|
+
}
|
|
869
|
+
const payload = objectPayload(await readJsonResponse(http, context), context, "compaction returned an invalid payload");
|
|
870
|
+
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
871
|
+
const first = choices[0] && typeof choices[0] === "object" ? choices[0] : {};
|
|
872
|
+
const message = first.message && typeof first.message === "object" ? first.message : {};
|
|
873
|
+
const summaryValue = typeof message.content === "string" ? message.content : chatContent(message.content);
|
|
874
|
+
const summary = typeof summaryValue === "string"
|
|
875
|
+
? summaryValue.trim()
|
|
876
|
+
: Array.isArray(summaryValue)
|
|
877
|
+
? summaryValue.map((part) => part && typeof part === "object" && typeof part.text === "string"
|
|
878
|
+
? part.text : "").join("\n").trim()
|
|
879
|
+
: "";
|
|
880
|
+
if (!summary)
|
|
881
|
+
throw providerError({ code: "malformed_response", message: "compaction returned no checkpoint text" }, { ...context, protocol: true });
|
|
882
|
+
const id = typeof payload.id === "string" ? payload.id : `compact_${Date.now().toString(36)}`;
|
|
883
|
+
const usage = normalizedUsage(payload.usage);
|
|
884
|
+
return {
|
|
885
|
+
id: `compact_${id}`,
|
|
886
|
+
object: "response.compaction",
|
|
887
|
+
output: [{
|
|
888
|
+
id: `msg_compact_${id}`, type: "message", role: "user", status: "completed",
|
|
889
|
+
content: [{ type: "input_text", text: `Conversation checkpoint generated by Looking Glass:\n${summary}` }],
|
|
890
|
+
}],
|
|
891
|
+
...(usage ? { usage } : {}),
|
|
892
|
+
};
|
|
893
|
+
}
|
|
406
894
|
if (this.config.gateway.provider === "lm-studio") {
|
|
407
895
|
const transcript = compactTranscript(request.input);
|
|
896
|
+
const compactContext = requestContext("lm-studio", "compact", this.apiKey, request.signal);
|
|
408
897
|
if (!transcript)
|
|
409
|
-
throw
|
|
898
|
+
throw providerError({ code: "malformed_response", message: "compaction received no semantic transcript" }, {
|
|
899
|
+
...compactContext, protocol: true,
|
|
900
|
+
});
|
|
410
901
|
const profile = {
|
|
411
902
|
model: request.model,
|
|
412
903
|
instructions: [
|
|
@@ -425,14 +916,44 @@ export class CodexLbClient {
|
|
|
425
916
|
promptCacheKey: request.promptCacheKey,
|
|
426
917
|
reasoningEffort: "none",
|
|
427
918
|
supportsReasoning: true,
|
|
919
|
+
supportsParallelToolCalls: false,
|
|
428
920
|
verbosity: "high",
|
|
429
921
|
fast: false,
|
|
430
922
|
...(request.signal ? { signal: request.signal } : {}),
|
|
431
923
|
};
|
|
432
|
-
const
|
|
924
|
+
const timeout = AbortSignal.timeout(this.config.gateway.timeoutMs);
|
|
925
|
+
const signal = request.signal ? AbortSignal.any([request.signal, timeout]) : timeout;
|
|
926
|
+
const context = requestContext("lm-studio", "compact", this.apiKey, request.signal, timeout);
|
|
927
|
+
let http;
|
|
928
|
+
try {
|
|
929
|
+
http = await fetch(`${this.config.gateway.baseURL.replace(/\/$/, "")}/responses`, {
|
|
930
|
+
method: "POST",
|
|
931
|
+
headers: {
|
|
932
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
933
|
+
"content-type": "application/json",
|
|
934
|
+
},
|
|
935
|
+
body: JSON.stringify(buildResponseParams("lm-studio", profile)),
|
|
936
|
+
signal,
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
catch (error) {
|
|
940
|
+
throw providerError(error, context);
|
|
941
|
+
}
|
|
942
|
+
const payload = await readJsonResponse(http, context);
|
|
943
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
944
|
+
throw providerError({ code: "malformed_response", message: "compaction returned an invalid payload" }, { ...context, protocol: true });
|
|
945
|
+
}
|
|
946
|
+
const response = payload;
|
|
947
|
+
if (!Array.isArray(response.output)) {
|
|
948
|
+
throw providerError({ code: "malformed_response", message: "compaction response did not contain output items" }, {
|
|
949
|
+
...context, protocol: true,
|
|
950
|
+
});
|
|
951
|
+
}
|
|
433
952
|
const summary = responseText(response).trim();
|
|
434
953
|
if (!summary)
|
|
435
|
-
throw
|
|
954
|
+
throw providerError({ code: "malformed_response", message: "compaction returned no checkpoint text" }, {
|
|
955
|
+
...context, protocol: true,
|
|
956
|
+
});
|
|
436
957
|
return {
|
|
437
958
|
id: `compact_${response.id}`,
|
|
438
959
|
object: "response.compaction",
|
|
@@ -458,29 +979,30 @@ export class CodexLbClient {
|
|
|
458
979
|
};
|
|
459
980
|
const timeout = AbortSignal.timeout(this.config.gateway.timeoutMs);
|
|
460
981
|
const signal = request.signal ? AbortSignal.any([request.signal, timeout]) : timeout;
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
const detail = detailFrom(payload);
|
|
473
|
-
throw Object.assign(new Error(this.safeErrorMessage(detail.message ?? `Compaction failed with HTTP ${response.status}`)), detail, {
|
|
474
|
-
status: response.status,
|
|
982
|
+
const context = requestContext(this.config.gateway.provider, "compact", this.apiKey, request.signal, timeout);
|
|
983
|
+
let response;
|
|
984
|
+
try {
|
|
985
|
+
response = await fetch(`${this.config.gateway.baseURL}/responses/compact`, {
|
|
986
|
+
method: "POST",
|
|
987
|
+
headers: {
|
|
988
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
989
|
+
"content-type": "application/json",
|
|
990
|
+
},
|
|
991
|
+
body: JSON.stringify(body),
|
|
992
|
+
signal,
|
|
475
993
|
});
|
|
476
994
|
}
|
|
995
|
+
catch (error) {
|
|
996
|
+
throw providerError(error, context);
|
|
997
|
+
}
|
|
998
|
+
const payload = await readJsonResponse(response, context);
|
|
477
999
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
478
|
-
throw
|
|
1000
|
+
throw providerError({ code: "malformed_response", message: "compaction returned an invalid payload" }, { ...context, protocol: true });
|
|
479
1001
|
}
|
|
480
1002
|
const output = payload.output;
|
|
481
1003
|
if (!Array.isArray(output) || output.length === 0
|
|
482
1004
|
|| output.some((item) => !item || typeof item !== "object" || Array.isArray(item))) {
|
|
483
|
-
throw
|
|
1005
|
+
throw providerError({ code: "malformed_response", message: "compaction response did not contain valid output items" }, { ...context, protocol: true });
|
|
484
1006
|
}
|
|
485
1007
|
return payload;
|
|
486
1008
|
}
|