@librechat/agents 3.7.19 → 3.7.21
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/dist/cjs/llm/openai/index.cjs +110 -24
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +1 -0
- package/dist/cjs/tools/BashExecutor.cjs +3 -2
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +27 -10
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +6 -5
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/diagnostics.cjs +26 -0
- package/dist/cjs/tools/diagnostics.cjs.map +1 -0
- package/dist/cjs/tools/search/format.cjs +7 -0
- package/dist/cjs/tools/search/format.cjs.map +1 -1
- package/dist/cjs/tools/search/rerankers.cjs +6 -4
- package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
- package/dist/cjs/tools/search/search.cjs +14 -2
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +8 -3
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +110 -25
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -2
- package/dist/esm/tools/BashExecutor.mjs +3 -2
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +2 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +27 -10
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +6 -5
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/diagnostics.mjs +25 -0
- package/dist/esm/tools/diagnostics.mjs.map +1 -0
- package/dist/esm/tools/search/format.mjs +7 -1
- package/dist/esm/tools/search/format.mjs.map +1 -1
- package/dist/esm/tools/search/rerankers.mjs +6 -5
- package/dist/esm/tools/search/rerankers.mjs.map +1 -1
- package/dist/esm/tools/search/search.mjs +14 -2
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +8 -4
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/types/llm/openai/index.d.ts +11 -1
- package/dist/types/tools/CodeExecutor.d.ts +13 -2
- package/dist/types/tools/diagnostics.d.ts +52 -0
- package/dist/types/tools/search/format.d.ts +2 -1
- package/dist/types/tools/search/rerankers.d.ts +10 -0
- package/dist/types/tools/search/tool.d.ts +1 -0
- package/dist/types/types/llm.d.ts +23 -0
- package/package.json +4 -2
- package/src/llm/openai/index.ts +281 -27
- package/src/tools/BashExecutor.ts +9 -4
- package/src/tools/BashProgrammaticToolCalling.ts +6 -3
- package/src/tools/CodeExecutor.ts +88 -15
- package/src/tools/ProgrammaticToolCalling.ts +24 -12
- package/src/tools/diagnostics.ts +107 -0
- package/src/tools/search/format.ts +15 -1
- package/src/tools/search/rerankers.ts +18 -3
- package/src/tools/search/search.ts +30 -5
- package/src/tools/search/tool.ts +13 -3
- package/src/types/llm.ts +23 -0
|
@@ -2,7 +2,13 @@ import { config } from 'dotenv';
|
|
|
2
2
|
import fetch, { RequestInit } from 'node-fetch';
|
|
3
3
|
import { getEnvironmentVariable } from '@langchain/core/utils/env';
|
|
4
4
|
import { tool, DynamicStructuredTool } from '@langchain/core/tools';
|
|
5
|
+
import type { Readable } from 'node:stream';
|
|
6
|
+
import type { CodeApiMethod } from '@/tools/diagnostics';
|
|
5
7
|
import type * as t from '@/types';
|
|
8
|
+
import {
|
|
9
|
+
describeCodeApiError,
|
|
10
|
+
logCodeApiDiagnostic,
|
|
11
|
+
} from '@/tools/diagnostics';
|
|
6
12
|
import { appendCodeSessionFileSummary } from '@/tools/CodeSessionFileSummary';
|
|
7
13
|
import { resolveFetchProxyAgent } from '@/utils/proxy';
|
|
8
14
|
import { INTENT_PROPERTY } from '@/tools/intentArg';
|
|
@@ -119,8 +125,7 @@ export const CodeExecutionToolSchema = {
|
|
|
119
125
|
required: ['lang', 'code'],
|
|
120
126
|
} as const;
|
|
121
127
|
|
|
122
|
-
export const CODE_API_EXPECTED_PROFILE_HEADER =
|
|
123
|
-
'X-CodeAPI-Expected-Profile';
|
|
128
|
+
export const CODE_API_EXPECTED_PROFILE_HEADER = 'X-CodeAPI-Expected-Profile';
|
|
124
129
|
|
|
125
130
|
type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];
|
|
126
131
|
|
|
@@ -221,7 +226,8 @@ export function buildCodeApiExecutionErrorMessage(response: {
|
|
|
221
226
|
}
|
|
222
227
|
|
|
223
228
|
export async function resolveCodeApiAuthHeaders(
|
|
224
|
-
authHeaders?: t.CodeApiAuthHeaders
|
|
229
|
+
authHeaders?: t.CodeApiAuthHeaders,
|
|
230
|
+
options?: { recoverable?: boolean }
|
|
225
231
|
): Promise<t.CodeApiAuthHeaderMap> {
|
|
226
232
|
if (authHeaders == null) {
|
|
227
233
|
return {};
|
|
@@ -230,7 +236,15 @@ export async function resolveCodeApiAuthHeaders(
|
|
|
230
236
|
try {
|
|
231
237
|
const resolvedHeaders = await authHeaders();
|
|
232
238
|
return resolvedHeaders;
|
|
233
|
-
} catch {
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (options?.recoverable !== true) {
|
|
241
|
+
logCodeApiDiagnostic(
|
|
242
|
+
'CodeExecutor',
|
|
243
|
+
'error',
|
|
244
|
+
'auth header resolution failed; the Code API request was never sent',
|
|
245
|
+
describeCodeApiError(error)
|
|
246
|
+
);
|
|
247
|
+
}
|
|
234
248
|
throw new CodeApiRequestError(CODE_API_AUTHORIZATION_ERROR_MESSAGE);
|
|
235
249
|
}
|
|
236
250
|
}
|
|
@@ -250,18 +264,73 @@ export function addCodeApiExecutionProfileHeader(
|
|
|
250
264
|
};
|
|
251
265
|
}
|
|
252
266
|
|
|
267
|
+
type CodeApiErrorResponse = {
|
|
268
|
+
status: number;
|
|
269
|
+
text: () => Promise<string>;
|
|
270
|
+
/** node-fetch types this as `NodeJS.ReadableStream`, which does not declare
|
|
271
|
+
* `destroy`; every runtime instance is a `Readable`, and a test double may
|
|
272
|
+
* supply nothing at all. */
|
|
273
|
+
body?: NodeJS.ReadableStream | null;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Only the 429 branch reads the body, and node-fetch keeps the stream and its
|
|
278
|
+
* socket alive until something does. Repeated backend failures would otherwise
|
|
279
|
+
* accumulate connections holding payloads this module deliberately discards.
|
|
280
|
+
*/
|
|
281
|
+
function discardResponseBody(response: CodeApiErrorResponse): void {
|
|
282
|
+
const body = response.body;
|
|
283
|
+
if (body == null || !('destroy' in body)) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const destroy = (body as NodeJS.ReadableStream & Partial<Readable>).destroy;
|
|
287
|
+
if (typeof destroy !== 'function') {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
destroy.call(body);
|
|
292
|
+
} catch {
|
|
293
|
+
/* Already finished or not destroyable; nothing is being held open. */
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
253
297
|
export async function buildCodeApiHttpErrorMessage(
|
|
254
|
-
|
|
298
|
+
method: CodeApiMethod,
|
|
255
299
|
_endpoint: string,
|
|
256
|
-
response:
|
|
300
|
+
response: CodeApiErrorResponse,
|
|
301
|
+
options?: { recoverable?: boolean; profile?: t.CodeApiExecutionProfile }
|
|
257
302
|
): Promise<string> {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
303
|
+
/* Logged before the body is touched. A non-OK response can leave a chunked
|
|
304
|
+
body open, and there is no read timeout here, so draining first would
|
|
305
|
+
withhold the diagnostic indefinitely for exactly the backend it identifies.
|
|
306
|
+
The body itself is never logged — it is upstream free text that can echo
|
|
307
|
+
the header that was sent — and the endpoint is host-configured, so the
|
|
308
|
+
backend is named by the profile this module chose rather than by its
|
|
309
|
+
address. A recoverable caller reports its own outcome; logging here too
|
|
310
|
+
would raise an error-level alert for a lookup that succeeds by returning
|
|
311
|
+
nothing. */
|
|
312
|
+
if (options?.recoverable !== true) {
|
|
313
|
+
logCodeApiDiagnostic(
|
|
314
|
+
'CodeExecutor',
|
|
315
|
+
response.status === 429 ? 'warn' : 'error',
|
|
316
|
+
'Code API rejected the request',
|
|
317
|
+
{
|
|
318
|
+
method,
|
|
319
|
+
profile: options?.profile ?? 'unset',
|
|
320
|
+
status: response.status,
|
|
321
|
+
}
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
if (response.status !== 429) {
|
|
325
|
+
discardResponseBody(response);
|
|
263
326
|
}
|
|
264
327
|
if (response.status === 429) {
|
|
328
|
+
let responseBody = '';
|
|
329
|
+
try {
|
|
330
|
+
responseBody = await response.text();
|
|
331
|
+
} catch {
|
|
332
|
+
responseBody = '';
|
|
333
|
+
}
|
|
265
334
|
const retryAfterSeconds = getRetryAfterSeconds(responseBody);
|
|
266
335
|
return retryAfterSeconds != null
|
|
267
336
|
? `Code execution is temporarily rate-limited. Retry after ${retryAfterSeconds} seconds.`
|
|
@@ -445,9 +514,11 @@ function createCodeExecutionTool(
|
|
|
445
514
|
session_id.length > 0 &&
|
|
446
515
|
!Array.isArray(postData.files)
|
|
447
516
|
) {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
517
|
+
logCodeApiDiagnostic(
|
|
518
|
+
'CodeExecutor',
|
|
519
|
+
'debug',
|
|
520
|
+
'session carried no injected files; exec will run without input files',
|
|
521
|
+
{ files: 'none' }
|
|
451
522
|
);
|
|
452
523
|
}
|
|
453
524
|
|
|
@@ -474,7 +545,9 @@ function createCodeExecutionTool(
|
|
|
474
545
|
const response = await fetch(execEndpoint, fetchOptions);
|
|
475
546
|
if (!response.ok) {
|
|
476
547
|
throw new CodeApiRequestError(
|
|
477
|
-
await buildCodeApiHttpErrorMessage('POST', execEndpoint, response
|
|
548
|
+
await buildCodeApiHttpErrorMessage('POST', execEndpoint, response, {
|
|
549
|
+
profile: executionProfile,
|
|
550
|
+
})
|
|
478
551
|
);
|
|
479
552
|
}
|
|
480
553
|
|
|
@@ -33,6 +33,10 @@ import {
|
|
|
33
33
|
createCodeApiRunTimeoutSchema,
|
|
34
34
|
resolveCodeApiRunTimeoutMs,
|
|
35
35
|
} from './ptcTimeout';
|
|
36
|
+
import {
|
|
37
|
+
describeCodeApiError,
|
|
38
|
+
logCodeApiDiagnostic,
|
|
39
|
+
} from '@/tools/diagnostics';
|
|
36
40
|
import { resolveFetchProxyAgent } from '@/utils/proxy';
|
|
37
41
|
import { INTENT_PROPERTY } from '@/tools/intentArg';
|
|
38
42
|
import { Constants } from '@/common';
|
|
@@ -449,7 +453,9 @@ export async function fetchSessionFiles(
|
|
|
449
453
|
}
|
|
450
454
|
}
|
|
451
455
|
const filesEndpoint = `${baseUrl}/files/${encodeURIComponent(sessionId)}?${query.toString()}`;
|
|
452
|
-
const resolvedAuthHeaders = await resolveCodeApiAuthHeaders(authHeaders
|
|
456
|
+
const resolvedAuthHeaders = await resolveCodeApiAuthHeaders(authHeaders, {
|
|
457
|
+
recoverable: true,
|
|
458
|
+
});
|
|
453
459
|
const fetchOptions: RequestInit = {
|
|
454
460
|
method: 'GET',
|
|
455
461
|
headers: {
|
|
@@ -466,7 +472,9 @@ export async function fetchSessionFiles(
|
|
|
466
472
|
const response = await fetch(filesEndpoint, fetchOptions);
|
|
467
473
|
if (!response.ok) {
|
|
468
474
|
throw new Error(
|
|
469
|
-
await buildCodeApiHttpErrorMessage('GET', filesEndpoint, response
|
|
475
|
+
await buildCodeApiHttpErrorMessage('GET', filesEndpoint, response, {
|
|
476
|
+
recoverable: true,
|
|
477
|
+
})
|
|
470
478
|
);
|
|
471
479
|
}
|
|
472
480
|
|
|
@@ -479,9 +487,11 @@ export async function fetchSessionFiles(
|
|
|
479
487
|
.filter(isCodeApiSessionFileWire)
|
|
480
488
|
.map((file) => normalizeSessionFile(file, sessionId, scope));
|
|
481
489
|
} catch (error) {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
490
|
+
logCodeApiDiagnostic(
|
|
491
|
+
'ProgrammaticToolCalling',
|
|
492
|
+
'warn',
|
|
493
|
+
'session file lookup failed; continuing without input files',
|
|
494
|
+
describeCodeApiError(error)
|
|
485
495
|
);
|
|
486
496
|
return [];
|
|
487
497
|
}
|
|
@@ -525,7 +535,9 @@ export async function makeRequest(
|
|
|
525
535
|
|
|
526
536
|
if (!response.ok) {
|
|
527
537
|
throw new CodeApiRequestError(
|
|
528
|
-
await buildCodeApiHttpErrorMessage('POST', endpoint, response
|
|
538
|
+
await buildCodeApiHttpErrorMessage('POST', endpoint, response, {
|
|
539
|
+
profile: executionProfile,
|
|
540
|
+
})
|
|
529
541
|
);
|
|
530
542
|
}
|
|
531
543
|
|
|
@@ -931,9 +943,7 @@ export async function runPlainExecution(args: {
|
|
|
931
943
|
{
|
|
932
944
|
lang: args.lang,
|
|
933
945
|
code:
|
|
934
|
-
args.lang === 'py'
|
|
935
|
-
? wrapPythonForPlainExecution(args.code)
|
|
936
|
-
: args.code,
|
|
946
|
+
args.lang === 'py' ? wrapPythonForPlainExecution(args.code) : args.code,
|
|
937
947
|
...(args.timeout != null ? { timeout: args.timeout } : {}),
|
|
938
948
|
...(args.sessionId != null && args.sessionId !== ''
|
|
939
949
|
? { session_id: args.sessionId }
|
|
@@ -1085,9 +1095,11 @@ export function createProgrammaticToolCallingTool(
|
|
|
1085
1095
|
if (_injected_files && _injected_files.length > 0) {
|
|
1086
1096
|
files = _injected_files;
|
|
1087
1097
|
} else if (session_id != null && session_id.length > 0) {
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1098
|
+
logCodeApiDiagnostic(
|
|
1099
|
+
'ProgrammaticToolCalling',
|
|
1100
|
+
'debug',
|
|
1101
|
+
'session carried no injected files; exec will run without input files',
|
|
1102
|
+
{ files: 'none' }
|
|
1091
1103
|
);
|
|
1092
1104
|
}
|
|
1093
1105
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type * as t from '@/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Operator diagnostics for the Code API tools.
|
|
5
|
+
*
|
|
6
|
+
* Every failure these tools surface to the model is reduced to a fixed
|
|
7
|
+
* sentence, so this log is the only surviving account of what went wrong —
|
|
8
|
+
* which also makes it the one place a credential can escape by accident. It
|
|
9
|
+
* escaped repeatedly while any field was quoted from something the tools did
|
|
10
|
+
* not produce: a response body echoing the header that was sent, an error
|
|
11
|
+
* message quoting an excerpt of a malformed signing key, a `stack` whose first
|
|
12
|
+
* line is that message, a writable `name`, a configured base URL carrying a
|
|
13
|
+
* capability in its path or its authority.
|
|
14
|
+
*
|
|
15
|
+
* Filtering those carriers case by case never converged, because the shape of
|
|
16
|
+
* a credential inside free text is not decidable. The rule here is structural
|
|
17
|
+
* instead: a diagnostic NAMES what happened using values this module owns, and
|
|
18
|
+
* QUOTES nothing it received. `CodeApiDiagnosticDetail` is a closed union of
|
|
19
|
+
* such values, so host-authored text cannot reach a log without failing to
|
|
20
|
+
* compile — and adding a field means widening that union in this file, which
|
|
21
|
+
* is the point at which the question gets asked.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export type CodeApiMethod = 'GET' | 'POST';
|
|
25
|
+
|
|
26
|
+
type CodeApiProfileLabel = t.CodeApiExecutionProfile | 'unset';
|
|
27
|
+
|
|
28
|
+
const KNOWN_ERROR_TYPES: ReadonlyArray<
|
|
29
|
+
readonly [CodeApiErrorLabel, new () => Error]
|
|
30
|
+
> = [
|
|
31
|
+
['SyntaxError', SyntaxError],
|
|
32
|
+
['TypeError', TypeError],
|
|
33
|
+
['RangeError', RangeError],
|
|
34
|
+
['ReferenceError', ReferenceError],
|
|
35
|
+
['URIError', URIError],
|
|
36
|
+
['EvalError', EvalError],
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** Built-in error types, the `typeof` results for a non-Error rejection, and
|
|
40
|
+
* the two labels this module supplies when neither applies. */
|
|
41
|
+
type CodeApiErrorLabel =
|
|
42
|
+
| 'SyntaxError'
|
|
43
|
+
| 'TypeError'
|
|
44
|
+
| 'RangeError'
|
|
45
|
+
| 'ReferenceError'
|
|
46
|
+
| 'URIError'
|
|
47
|
+
| 'EvalError'
|
|
48
|
+
| 'Error'
|
|
49
|
+
| 'UndescribableError'
|
|
50
|
+
| 'string'
|
|
51
|
+
| 'number'
|
|
52
|
+
| 'bigint'
|
|
53
|
+
| 'boolean'
|
|
54
|
+
| 'symbol'
|
|
55
|
+
| 'undefined'
|
|
56
|
+
| 'object'
|
|
57
|
+
| 'function';
|
|
58
|
+
|
|
59
|
+
export type CodeApiDiagnosticDetail =
|
|
60
|
+
| { type: CodeApiErrorLabel }
|
|
61
|
+
| { method: CodeApiMethod; profile: CodeApiProfileLabel; status: number }
|
|
62
|
+
| { files: 'none' };
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Classifies a rejection without reading anything off it. `instanceof` walks
|
|
66
|
+
* the prototype chain, so an accessor trap is never reached; the guard covers
|
|
67
|
+
* a `getPrototypeOf` trap, which would otherwise cost both the log and the
|
|
68
|
+
* rejection.
|
|
69
|
+
*/
|
|
70
|
+
export function describeCodeApiError(error: unknown): {
|
|
71
|
+
type: CodeApiErrorLabel;
|
|
72
|
+
} {
|
|
73
|
+
try {
|
|
74
|
+
if (!(error instanceof Error)) {
|
|
75
|
+
return { type: typeof error };
|
|
76
|
+
}
|
|
77
|
+
for (const [label, constructor] of KNOWN_ERROR_TYPES) {
|
|
78
|
+
if (error instanceof constructor) {
|
|
79
|
+
return { type: label };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { type: 'Error' };
|
|
83
|
+
} catch {
|
|
84
|
+
return { type: 'UndescribableError' };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type CodeApiDiagnosticSource =
|
|
89
|
+
| 'CodeExecutor'
|
|
90
|
+
| 'BashExecutor'
|
|
91
|
+
| 'ProgrammaticToolCalling'
|
|
92
|
+
| 'BashProgrammaticToolCalling';
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Console is the SDK's diagnostic channel; a winston logger is the host's
|
|
96
|
+
* concern. Hosts correlate these lines with their own request-scoped logs,
|
|
97
|
+
* which is where received identifiers such as a session id belong.
|
|
98
|
+
*/
|
|
99
|
+
export function logCodeApiDiagnostic(
|
|
100
|
+
source: CodeApiDiagnosticSource,
|
|
101
|
+
level: 'debug' | 'warn' | 'error',
|
|
102
|
+
message: string,
|
|
103
|
+
detail: CodeApiDiagnosticDetail
|
|
104
|
+
): void {
|
|
105
|
+
// eslint-disable-next-line no-console
|
|
106
|
+
console[level](`[${source}] ${message}`, detail);
|
|
107
|
+
}
|
|
@@ -11,6 +11,9 @@ const DEFAULT_MAX_LLM_OUTPUT_CHARS = 50000;
|
|
|
11
11
|
* this we drop it whole rather than emit a useless sliver. */
|
|
12
12
|
const MIN_PARTIAL_HIGHLIGHT_CHARS = 200;
|
|
13
13
|
|
|
14
|
+
export const MISSING_SEARCH_RESULT_DATA_ERROR =
|
|
15
|
+
'Search provider returned no result data';
|
|
16
|
+
|
|
14
17
|
/** Resolves the per-search highlight budget from config, the
|
|
15
18
|
* `SEARCH_MAX_LLM_OUTPUT_CHARS` env var, or the default (50,000 chars). */
|
|
16
19
|
export function resolveMaxLLMOutputChars(maxOutputChars?: number): number {
|
|
@@ -219,9 +222,16 @@ function formatSource(
|
|
|
219
222
|
|
|
220
223
|
export function formatResultsForLLM(
|
|
221
224
|
turn: number,
|
|
222
|
-
results
|
|
225
|
+
results?: t.SearchResultData | null,
|
|
223
226
|
maxOutputChars?: number
|
|
224
227
|
): { output: string; references: t.ResultReference[] } {
|
|
228
|
+
if (results == null) {
|
|
229
|
+
return {
|
|
230
|
+
output: `Search failed: ${MISSING_SEARCH_RESULT_DATA_ERROR}`,
|
|
231
|
+
references: [],
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
225
235
|
/** Bound highlight content to the per-search budget before formatting */
|
|
226
236
|
const trimmedHighlights = trimHighlightsToBudget(
|
|
227
237
|
results,
|
|
@@ -239,6 +249,10 @@ export function formatResultsForLLM(
|
|
|
239
249
|
|
|
240
250
|
const references: t.ResultReference[] = [];
|
|
241
251
|
|
|
252
|
+
if (results.error != null && results.error !== '') {
|
|
253
|
+
outputLines.push(`Search failed: ${results.error}`);
|
|
254
|
+
}
|
|
255
|
+
|
|
242
256
|
// Organic (web) results
|
|
243
257
|
if (results.organic?.length != null && results.organic.length > 0) {
|
|
244
258
|
addSection(`Web Results, Turn ${turn}`);
|
|
@@ -21,6 +21,23 @@ const getDefaultCohereApiUrl = (): string =>
|
|
|
21
21
|
? process.env.COHERE_API_URL
|
|
22
22
|
: DEFAULT_COHERE_API_URL;
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Ranking used whenever no reranker scores the chunks: the candidates' own
|
|
26
|
+
* order, capped at `topK`, with a neutral score.
|
|
27
|
+
*
|
|
28
|
+
* Exported because it is not only a *fallback*. With `rerankerType: 'none'`
|
|
29
|
+
* there is no reranker to fall back from, and the search pipeline needs the
|
|
30
|
+
* same ranking to hand the scraped text downstream — see `getHighlights` in
|
|
31
|
+
* `./search`.
|
|
32
|
+
*/
|
|
33
|
+
export const getDefaultRanking = (
|
|
34
|
+
documents: string[],
|
|
35
|
+
topK: number
|
|
36
|
+
): t.Highlight[] =>
|
|
37
|
+
documents
|
|
38
|
+
.slice(0, Math.min(topK, documents.length))
|
|
39
|
+
.map((doc) => ({ text: doc, score: 0 }));
|
|
40
|
+
|
|
24
41
|
export abstract class BaseReranker {
|
|
25
42
|
protected apiKey: string | undefined;
|
|
26
43
|
protected logger: t.Logger;
|
|
@@ -51,9 +68,7 @@ export abstract class BaseReranker {
|
|
|
51
68
|
documents: string[],
|
|
52
69
|
topK: number
|
|
53
70
|
): t.Highlight[] {
|
|
54
|
-
return documents
|
|
55
|
-
.slice(0, Math.min(topK, documents.length))
|
|
56
|
-
.map((doc) => ({ text: doc, score: 0 }));
|
|
71
|
+
return getDefaultRanking(documents, topK);
|
|
57
72
|
}
|
|
58
73
|
|
|
59
74
|
/** A direct caller has no enclosing search to fold into, so one
|
|
@@ -10,7 +10,7 @@ import { createKeenableAPI } from './keenable-search';
|
|
|
10
10
|
import { createTavilyAPI } from './tavily-search';
|
|
11
11
|
import { createSearchMetrics } from './metrics';
|
|
12
12
|
import { createCrwAPI } from './crw-search';
|
|
13
|
-
import { BaseReranker } from './rerankers';
|
|
13
|
+
import { BaseReranker, getDefaultRanking } from './rerankers';
|
|
14
14
|
|
|
15
15
|
/** Engines queried when `searxngSearchOptions.engines` is not configured. */
|
|
16
16
|
const DEFAULT_SEARXNG_ENGINES = 'google,bing,duckduckgo';
|
|
@@ -144,9 +144,20 @@ function createSourceUpdateCallback(sourceMap: Map<string, t.ValidSource>) {
|
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/** Provider label for the metrics summary when reranking is switched off.
|
|
148
|
+
* `undefined` would be indistinguishable from a reranker that never ran. */
|
|
149
|
+
const NO_RERANKER_PROVIDER = 'none';
|
|
150
|
+
|
|
147
151
|
/** Returns undefined without logging when there is nothing to rank: an empty
|
|
148
|
-
* scrape is already counted by the scrape summary
|
|
149
|
-
*
|
|
152
|
+
* scrape is already counted by the scrape summary.
|
|
153
|
+
*
|
|
154
|
+
* A *missing* reranker is not the same as nothing to rank. `rerankerType:
|
|
155
|
+
* 'none'` is a schema-valid opt-out, and `createReranker` returns `undefined`
|
|
156
|
+
* for it by design. Returning `undefined` here as well would leave the source
|
|
157
|
+
* without highlights, and `expandHighlights` then strips its content — the
|
|
158
|
+
* search would answer with links and no text at all. So the chunks are handed
|
|
159
|
+
* on in their original order via `getDefaultRanking`, exactly what every
|
|
160
|
+
* reranker's own failure path already does. */
|
|
150
161
|
const getHighlights = async ({
|
|
151
162
|
query,
|
|
152
163
|
content,
|
|
@@ -164,7 +175,7 @@ const getHighlights = async ({
|
|
|
164
175
|
maxContentLength?: number;
|
|
165
176
|
chunkOptions?: { chunkSize: number; chunkOverlap: number };
|
|
166
177
|
}): Promise<t.Highlight[] | undefined> => {
|
|
167
|
-
if (!content
|
|
178
|
+
if (!content) {
|
|
168
179
|
return;
|
|
169
180
|
}
|
|
170
181
|
|
|
@@ -181,7 +192,7 @@ const getHighlights = async ({
|
|
|
181
192
|
);
|
|
182
193
|
} catch (error) {
|
|
183
194
|
metrics.recordRerank({
|
|
184
|
-
provider: reranker
|
|
195
|
+
provider: reranker?.provider ?? NO_RERANKER_PROVIDER,
|
|
185
196
|
chunks: 0,
|
|
186
197
|
results: 0,
|
|
187
198
|
durationMs: Date.now() - chunkStartedAt,
|
|
@@ -191,6 +202,20 @@ const getHighlights = async ({
|
|
|
191
202
|
return;
|
|
192
203
|
}
|
|
193
204
|
|
|
205
|
+
/** Reranking is switched off, so the chunks pass through unscored. Recorded
|
|
206
|
+
* like any other rerank so the search summary still accounts for the source;
|
|
207
|
+
* it carries no `reason`, because nothing failed. */
|
|
208
|
+
if (!reranker) {
|
|
209
|
+
const highlights = getDefaultRanking(documents, topResults);
|
|
210
|
+
metrics.recordRerank({
|
|
211
|
+
provider: NO_RERANKER_PROVIDER,
|
|
212
|
+
chunks: documents.length,
|
|
213
|
+
results: highlights.length,
|
|
214
|
+
durationMs: Date.now() - chunkStartedAt,
|
|
215
|
+
});
|
|
216
|
+
return highlights;
|
|
217
|
+
}
|
|
218
|
+
|
|
194
219
|
const rerankStartedAt = Date.now();
|
|
195
220
|
try {
|
|
196
221
|
return await reranker.rerank(query, documents, topResults, metrics);
|
package/src/tools/search/tool.ts
CHANGED
|
@@ -21,7 +21,10 @@ import { INTENT_PROPERTY } from '@/tools/intentArg';
|
|
|
21
21
|
import { createCrwScraper } from './crw-scraper';
|
|
22
22
|
import { expandHighlights } from './highlights';
|
|
23
23
|
import { createSearchMetrics } from './metrics';
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
formatResultsForLLM,
|
|
26
|
+
MISSING_SEARCH_RESULT_DATA_ERROR,
|
|
27
|
+
} from './format';
|
|
25
28
|
import { createDefaultLogger } from './utils';
|
|
26
29
|
import { createReranker } from './rerankers';
|
|
27
30
|
import { Constants } from '@/common';
|
|
@@ -66,6 +69,12 @@ export function resolveSearchOutcome(
|
|
|
66
69
|
return `Found ${count} result${count === 1 ? '' : 's'} for "${query}"`;
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
export function normalizeSearchResultData(
|
|
73
|
+
result: t.SearchResultData | null | undefined
|
|
74
|
+
): t.SearchResultData {
|
|
75
|
+
return result ?? { error: MISSING_SEARCH_RESULT_DATA_ERROR };
|
|
76
|
+
}
|
|
77
|
+
|
|
69
78
|
/** Distinct rows across the main search's two collections. SearXNG derives
|
|
70
79
|
* both from one result array — a row matching its news heuristic lands in
|
|
71
80
|
* `organic` and `topStories` alike — so summing the lengths would report
|
|
@@ -431,12 +440,13 @@ function createTool({
|
|
|
431
440
|
}),
|
|
432
441
|
});
|
|
433
442
|
const turn = runnableConfig.toolCall?.turn ?? 0;
|
|
443
|
+
const resultData = normalizeSearchResultData(searchResult);
|
|
434
444
|
const { output, references } = formatResultsForLLM(
|
|
435
445
|
turn,
|
|
436
|
-
|
|
446
|
+
resultData,
|
|
437
447
|
maxOutputChars
|
|
438
448
|
);
|
|
439
|
-
const data: t.SearchResultData = { turn, ...
|
|
449
|
+
const data: t.SearchResultData = { turn, ...resultData, references };
|
|
440
450
|
const outcome = resolveSearchOutcome(data, query);
|
|
441
451
|
return [
|
|
442
452
|
output,
|
package/src/types/llm.ts
CHANGED
|
@@ -81,6 +81,29 @@ export type GoogleThinkingConfig = {
|
|
|
81
81
|
export type ManagedRequestOptions = {
|
|
82
82
|
promptCacheExplicit?: boolean;
|
|
83
83
|
safety_identifier?: string;
|
|
84
|
+
/**
|
|
85
|
+
* Declares that this client talks to the first-party OpenAI or Azure surface.
|
|
86
|
+
* Gates the model-specific request *shaping* documented only for it —
|
|
87
|
+
* currently GPT-6 Astra's rejected sampling and logprob parameters, its
|
|
88
|
+
* unsupported reasoning efforts, and the encrypted reasoning it supports —
|
|
89
|
+
* and defaults to off.
|
|
90
|
+
*
|
|
91
|
+
* Shaping only. Which API serves the turn is not decided here: GPT-6 Astra
|
|
92
|
+
* serves tool calls only from the Responses API, and a caller wanting them
|
|
93
|
+
* must select it with `useResponsesApi`, alongside the rest of the request
|
|
94
|
+
* shaping that depends on which API is in use.
|
|
95
|
+
*
|
|
96
|
+
* The shaping runs inside this SDK's own request delegates. A caller that
|
|
97
|
+
* supplies its own `completions` or `responses` delegate replaces that
|
|
98
|
+
* construction and owns the request shaping for it — this flag cannot reach
|
|
99
|
+
* inside a delegate it did not build.
|
|
100
|
+
*
|
|
101
|
+
* Declared rather than inferred from a base URL: only the caller knows
|
|
102
|
+
* whether a URL is a faithful first-party route, a gateway, or a proxy with
|
|
103
|
+
* its own semantics, and every gate it controls removes capability, so
|
|
104
|
+
* guessing wrong silently degrades an endpoint the SDK cannot see.
|
|
105
|
+
*/
|
|
106
|
+
firstPartyEndpoint?: boolean;
|
|
84
107
|
};
|
|
85
108
|
/**
|
|
86
109
|
* Adaptive stream-smoothing configuration shared by every provider client.
|