@sigil0/looking-glass 0.2.4 → 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 +632 -82
- 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,14 +1,43 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { providerError, providerHttpError } from "../errors.js";
|
|
2
|
+
// Current llama.cpp grammar generation can emit invalid GBNF for nested string
|
|
3
|
+
// maxLength constraints at or above 2000. Keep the original schemas for local
|
|
4
|
+
// validation and only relax the cloned schema sent to LM Studio.
|
|
5
|
+
function omitLmStudioLongMaxLengths(value) {
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
for (const item of value)
|
|
8
|
+
omitLmStudioLongMaxLengths(item);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (!value || typeof value !== "object")
|
|
12
|
+
return;
|
|
13
|
+
const object = value;
|
|
14
|
+
for (const [key, child] of Object.entries(object)) {
|
|
15
|
+
if (key === "maxLength" && typeof child === "number" && child >= 2_000) {
|
|
16
|
+
delete object[key];
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
omitLmStudioLongMaxLengths(child);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function lmStudioTools(tools) {
|
|
23
|
+
return tools.map((tool) => {
|
|
24
|
+
const parameters = tool.parameters === null ? null : structuredClone(tool.parameters);
|
|
25
|
+
if (parameters !== null)
|
|
26
|
+
omitLmStudioLongMaxLengths(parameters);
|
|
27
|
+
return { ...tool, parameters };
|
|
28
|
+
});
|
|
29
|
+
}
|
|
3
30
|
export function buildResponseParams(provider, request) {
|
|
31
|
+
if (provider === "openrouter")
|
|
32
|
+
return buildOpenRouterParams(request);
|
|
4
33
|
const common = {
|
|
5
34
|
model: request.model,
|
|
6
35
|
instructions: request.instructions,
|
|
7
36
|
input: provider === "lm-studio"
|
|
8
37
|
? request.input.filter((item) => item.type !== "reasoning")
|
|
9
38
|
: request.input,
|
|
10
|
-
tools: request.tools,
|
|
11
|
-
parallel_tool_calls: true,
|
|
39
|
+
tools: provider === "lm-studio" ? lmStudioTools(request.tools) : request.tools,
|
|
40
|
+
...(request.supportsParallelToolCalls ? { parallel_tool_calls: true } : {}),
|
|
12
41
|
...(request.supportsReasoning ? {
|
|
13
42
|
reasoning: {
|
|
14
43
|
// codex-lb advertises "ultra" before the public SDK type includes it.
|
|
@@ -38,14 +67,14 @@ function detailFrom(value) {
|
|
|
38
67
|
const outer = value;
|
|
39
68
|
return outer.error && typeof outer.error === "object" ? outer.error : outer;
|
|
40
69
|
}
|
|
41
|
-
async function* responseEvents(response) {
|
|
70
|
+
async function* responseEvents(response, context) {
|
|
42
71
|
if (!response.body)
|
|
43
|
-
throw
|
|
44
|
-
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();
|
|
45
74
|
const decoder = new TextDecoder();
|
|
46
75
|
let buffer = "";
|
|
47
76
|
let data = [];
|
|
48
|
-
const
|
|
77
|
+
const parseResponseEvent = () => {
|
|
49
78
|
const payload = data.join("\n");
|
|
50
79
|
data = [];
|
|
51
80
|
if (!payload || payload === "[DONE]")
|
|
@@ -58,20 +87,20 @@ async function* responseEvents(response) {
|
|
|
58
87
|
return event;
|
|
59
88
|
}
|
|
60
89
|
catch (error) {
|
|
61
|
-
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)}`), {
|
|
62
91
|
code: "malformed_response_event",
|
|
63
|
-
});
|
|
92
|
+
}), { ...(context ?? { provider: "provider", operation: "stream" }), protocol: true });
|
|
64
93
|
}
|
|
65
94
|
};
|
|
66
95
|
while (true) {
|
|
67
|
-
const chunk = await
|
|
96
|
+
const chunk = context ? await readStreamChunk(streamReader, context) : await streamReader.read();
|
|
68
97
|
buffer += decoder.decode(chunk.value ?? new Uint8Array(), { stream: !chunk.done });
|
|
69
98
|
let newline = buffer.indexOf("\n");
|
|
70
99
|
while (newline >= 0) {
|
|
71
100
|
const line = buffer.slice(0, newline).replace(/\r$/, "");
|
|
72
101
|
buffer = buffer.slice(newline + 1);
|
|
73
102
|
if (line === "") {
|
|
74
|
-
const event =
|
|
103
|
+
const event = parseResponseEvent();
|
|
75
104
|
if (event)
|
|
76
105
|
yield event;
|
|
77
106
|
}
|
|
@@ -85,7 +114,7 @@ async function* responseEvents(response) {
|
|
|
85
114
|
}
|
|
86
115
|
if (buffer.startsWith("data:"))
|
|
87
116
|
data.push(buffer.slice(5).trimStart());
|
|
88
|
-
const event =
|
|
117
|
+
const event = parseResponseEvent();
|
|
89
118
|
if (event)
|
|
90
119
|
yield event;
|
|
91
120
|
}
|
|
@@ -175,6 +204,42 @@ export function lmStudioModelInfo(raw) {
|
|
|
175
204
|
priority: raw.loaded_instances?.length ? 0 : 1_000,
|
|
176
205
|
};
|
|
177
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
|
+
}
|
|
178
243
|
export function responseText(response) {
|
|
179
244
|
if (response.output_text)
|
|
180
245
|
return response.output_text;
|
|
@@ -229,40 +294,285 @@ function lmStudioModelsURL(baseURL) {
|
|
|
229
294
|
url.pathname = `${url.pathname.replace(/\/?v1\/?$/, "")}/api/v1/models`.replace(/\/+/g, "/");
|
|
230
295
|
return url.toString();
|
|
231
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
|
+
}
|
|
232
551
|
export class CodexLbClient {
|
|
233
552
|
config;
|
|
234
|
-
client;
|
|
235
553
|
apiKey;
|
|
236
554
|
constructor(config) {
|
|
237
555
|
this.config = config;
|
|
238
556
|
this.apiKey = process.env[config.gateway.apiKeyEnv] || "local-looking-glass";
|
|
239
|
-
this.client = new OpenAI({
|
|
240
|
-
apiKey: this.apiKey,
|
|
241
|
-
baseURL: config.gateway.baseURL,
|
|
242
|
-
maxRetries: 0,
|
|
243
|
-
timeout: config.gateway.timeoutMs,
|
|
244
|
-
});
|
|
245
557
|
}
|
|
246
558
|
supportsResponseContinuity() {
|
|
247
559
|
return this.config.gateway.provider === "codex-lb";
|
|
248
560
|
}
|
|
249
|
-
safeErrorMessage(message) {
|
|
250
|
-
return redactSensitiveText(message, [this.apiKey]);
|
|
251
|
-
}
|
|
252
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);
|
|
253
566
|
if (this.config.gateway.provider === "lm-studio") {
|
|
254
567
|
try {
|
|
255
|
-
const timeout = AbortSignal.timeout(this.config.gateway.timeoutMs);
|
|
256
|
-
const requestSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
257
568
|
const response = await fetch(lmStudioModelsURL(this.config.gateway.baseURL), {
|
|
258
569
|
headers: { authorization: `Bearer ${this.apiKey}` },
|
|
259
570
|
signal: requestSignal,
|
|
260
571
|
});
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
.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")
|
|
266
576
|
.map(lmStudioModelInfo)
|
|
267
577
|
.sort((left, right) => left.priority - right.priority || left.name.localeCompare(right.name));
|
|
268
578
|
if (models.length > 0)
|
|
@@ -274,47 +584,196 @@ export class CodexLbClient {
|
|
|
274
584
|
// Older LM Studio versions and restricted tokens may only expose /v1/models.
|
|
275
585
|
}
|
|
276
586
|
}
|
|
277
|
-
|
|
278
|
-
|
|
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
|
|
279
608
|
.filter((model) => model.metadata?.supported_in_api !== false)
|
|
280
609
|
.map(modelInfo)
|
|
281
610
|
.sort((left, right) => left.priority - right.priority || left.name.localeCompare(right.name));
|
|
282
611
|
}
|
|
283
|
-
async
|
|
284
|
-
const params = buildResponseParams(this.config.gateway.provider, request);
|
|
612
|
+
async openRouterStream(request, callbacks) {
|
|
285
613
|
const timeout = AbortSignal.timeout(this.config.gateway.timeoutMs);
|
|
286
614
|
const signal = request.signal ? AbortSignal.any([request.signal, timeout]) : timeout;
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
+
}
|
|
297
632
|
if (!http.ok) {
|
|
298
|
-
const text = await http
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
+
}
|
|
305
704
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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,
|
|
729
|
+
});
|
|
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,
|
|
310
763
|
});
|
|
311
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
|
+
}
|
|
312
771
|
let created = {};
|
|
313
772
|
let terminal = null;
|
|
314
773
|
let streamedError = null;
|
|
315
774
|
let sawTerminal = false;
|
|
316
775
|
const output = new Map();
|
|
317
|
-
for await (const event of responseEvents(http)) {
|
|
776
|
+
for await (const event of responseEvents(http, context)) {
|
|
318
777
|
callbacks.onEvent?.(event);
|
|
319
778
|
if (event.type === "response.output_text.delta" && typeof event.delta === "string") {
|
|
320
779
|
callbacks.onTextDelta?.(event.delta);
|
|
@@ -342,10 +801,9 @@ export class CodexLbClient {
|
|
|
342
801
|
}
|
|
343
802
|
if (!sawTerminal) {
|
|
344
803
|
if (streamedError) {
|
|
345
|
-
|
|
346
|
-
throw Object.assign(new Error(this.safeErrorMessage(detail.message ?? "Response stream failed")), detail);
|
|
804
|
+
throw providerError(streamedError, context);
|
|
347
805
|
}
|
|
348
|
-
throw
|
|
806
|
+
throw providerError({ code: "stream_incomplete", message: "response stream ended without a terminal event" }, context);
|
|
349
807
|
}
|
|
350
808
|
const combined = { ...created, ...(terminal ?? {}) };
|
|
351
809
|
const terminalOutput = Array.isArray(combined.output) ? combined.output : [];
|
|
@@ -356,12 +814,15 @@ export class CodexLbClient {
|
|
|
356
814
|
? combined.status
|
|
357
815
|
: streamedError ? "failed" : "completed";
|
|
358
816
|
if (streamedError || status === "failed" || status === "incomplete") {
|
|
359
|
-
|
|
360
|
-
|
|
817
|
+
throw providerError(streamedError ?? combined.error ?? combined.incomplete_details ?? { message: `Response ${status}` }, {
|
|
818
|
+
...context,
|
|
819
|
+
responseStatus: status,
|
|
820
|
+
});
|
|
361
821
|
}
|
|
362
822
|
if (typeof combined.id !== "string" || !combined.id || canonicalOutput.length === 0) {
|
|
363
|
-
throw
|
|
364
|
-
|
|
823
|
+
throw providerError({ code: "malformed_response", message: "response stream returned an invalid completed response" }, {
|
|
824
|
+
...context,
|
|
825
|
+
protocol: true,
|
|
365
826
|
});
|
|
366
827
|
}
|
|
367
828
|
const canonical = {
|
|
@@ -375,10 +836,68 @@ export class CodexLbClient {
|
|
|
375
836
|
return this.config.gateway.provider === "lm-studio" ? redactLmStudioReasoning(canonical) : canonical;
|
|
376
837
|
}
|
|
377
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
|
+
}
|
|
378
894
|
if (this.config.gateway.provider === "lm-studio") {
|
|
379
895
|
const transcript = compactTranscript(request.input);
|
|
896
|
+
const compactContext = requestContext("lm-studio", "compact", this.apiKey, request.signal);
|
|
380
897
|
if (!transcript)
|
|
381
|
-
throw
|
|
898
|
+
throw providerError({ code: "malformed_response", message: "compaction received no semantic transcript" }, {
|
|
899
|
+
...compactContext, protocol: true,
|
|
900
|
+
});
|
|
382
901
|
const profile = {
|
|
383
902
|
model: request.model,
|
|
384
903
|
instructions: [
|
|
@@ -397,14 +916,44 @@ export class CodexLbClient {
|
|
|
397
916
|
promptCacheKey: request.promptCacheKey,
|
|
398
917
|
reasoningEffort: "none",
|
|
399
918
|
supportsReasoning: true,
|
|
919
|
+
supportsParallelToolCalls: false,
|
|
400
920
|
verbosity: "high",
|
|
401
921
|
fast: false,
|
|
402
922
|
...(request.signal ? { signal: request.signal } : {}),
|
|
403
923
|
};
|
|
404
|
-
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
|
+
}
|
|
405
952
|
const summary = responseText(response).trim();
|
|
406
953
|
if (!summary)
|
|
407
|
-
throw
|
|
954
|
+
throw providerError({ code: "malformed_response", message: "compaction returned no checkpoint text" }, {
|
|
955
|
+
...context, protocol: true,
|
|
956
|
+
});
|
|
408
957
|
return {
|
|
409
958
|
id: `compact_${response.id}`,
|
|
410
959
|
object: "response.compaction",
|
|
@@ -430,29 +979,30 @@ export class CodexLbClient {
|
|
|
430
979
|
};
|
|
431
980
|
const timeout = AbortSignal.timeout(this.config.gateway.timeoutMs);
|
|
432
981
|
const signal = request.signal ? AbortSignal.any([request.signal, timeout]) : timeout;
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const detail = detailFrom(payload);
|
|
445
|
-
throw Object.assign(new Error(this.safeErrorMessage(detail.message ?? `Compaction failed with HTTP ${response.status}`)), detail, {
|
|
446
|
-
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,
|
|
447
993
|
});
|
|
448
994
|
}
|
|
995
|
+
catch (error) {
|
|
996
|
+
throw providerError(error, context);
|
|
997
|
+
}
|
|
998
|
+
const payload = await readJsonResponse(response, context);
|
|
449
999
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
450
|
-
throw
|
|
1000
|
+
throw providerError({ code: "malformed_response", message: "compaction returned an invalid payload" }, { ...context, protocol: true });
|
|
451
1001
|
}
|
|
452
1002
|
const output = payload.output;
|
|
453
1003
|
if (!Array.isArray(output) || output.length === 0
|
|
454
1004
|
|| output.some((item) => !item || typeof item !== "object" || Array.isArray(item))) {
|
|
455
|
-
throw
|
|
1005
|
+
throw providerError({ code: "malformed_response", message: "compaction response did not contain valid output items" }, { ...context, protocol: true });
|
|
456
1006
|
}
|
|
457
1007
|
return payload;
|
|
458
1008
|
}
|