@juspay/neurolink 12.12.6 → 12.12.8
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/CHANGELOG.md +3 -2
- package/dist/browser/neurolink.min.js +405 -407
- package/dist/cli/commands/proxy.js +62 -24
- package/dist/cli/commands/proxyAnalyze.js +4 -1
- package/dist/core/toolExecutionRecorder.d.ts +16 -1
- package/dist/core/toolExecutionRecorder.js +21 -0
- package/dist/middleware/builtin/guardrails.js +67 -17
- package/dist/neurolink.js +6 -0
- package/dist/providers/amazonSagemaker.js +2 -1
- package/dist/providers/anthropic/client.js +2 -1
- package/dist/providers/openaiChatCompletionsBase.js +2 -1
- package/dist/proxy/codexUsage.d.ts +2 -1
- package/dist/proxy/codexUsage.js +82 -33
- package/dist/proxy/proxyActivity.d.ts +4 -1
- package/dist/proxy/proxyActivity.js +40 -14
- package/dist/proxy/proxyAnalysis.js +184 -59
- package/dist/proxy/proxyLifecycle.d.ts +1 -1
- package/dist/proxy/proxyLifecycle.js +54 -8
- package/dist/proxy/requestLogger.d.ts +2 -1
- package/dist/proxy/requestLogger.js +87 -29
- package/dist/proxy/sseInterceptor.js +36 -18
- package/dist/proxy/streamOutcome.d.ts +1 -1
- package/dist/proxy/streamOutcome.js +7 -1
- package/dist/server/routes/claudeProxyRoutes.js +70 -16
- package/dist/server/routes/codexProxyRoutes.js +73 -8
- package/dist/types/generate.d.ts +6 -0
- package/dist/types/middleware.d.ts +1 -1
- package/dist/types/proxy.d.ts +69 -3
- package/package.json +3 -1
|
@@ -26,7 +26,7 @@ import { ProxyRuntimeConfigStore } from "../../proxy/runtimeConfig.js";
|
|
|
26
26
|
import { startProxyLogCleanupScheduler } from "../../proxy/logCleanupScheduler.js";
|
|
27
27
|
import { anthropicAccountKeysEqual, createAccountAllowlist, isAccountAllowed, LEGACY_ANTHROPIC_ACCOUNT_KEY, normalizeAnthropicAccountKey, shouldLoadFallbackCredential, } from "../../proxy/accountSelection.js";
|
|
28
28
|
import { resolveProxyStatusAccountIdentity } from "../../proxy/codexAccountUsage.js";
|
|
29
|
-
import { beginProxyRequest, getProxyActivitySnapshot, takeProxyResponseObservers, trackProxyResponse, } from "../../proxy/proxyActivity.js";
|
|
29
|
+
import { beginProxyRequest, getProxyActivitySnapshot, observeProxyFinalLog, takeProxyResponseObservers, trackProxyResponse, } from "../../proxy/proxyActivity.js";
|
|
30
30
|
import { flushProxyLifecycleEvents, getProxyLifecycleLoggerSnapshot, hashProxyLifecycleSessionId, logProxyLifecycleEvent, } from "../../proxy/proxyLifecycle.js";
|
|
31
31
|
import { describeInstallFailure, getGlobalInstallArgs, isTransientInstallFailure, resolveGlobalInstaller, validateInstalledVersion, } from "../../proxy/globalInstaller.js";
|
|
32
32
|
import { startUpdaterWorkerSupervisor } from "../../proxy/updaterSupervisor.js";
|
|
@@ -1079,10 +1079,14 @@ function registerProxyRequestTracking(app, requestMetadata, readiness) {
|
|
|
1079
1079
|
rejectForUpdate: readiness.drainingForUpdate,
|
|
1080
1080
|
};
|
|
1081
1081
|
requestMetadata.set(c.req.raw, metadata);
|
|
1082
|
+
const stopObservingFinalLog = observeProxyFinalLog(metadata.requestId, (entry) => {
|
|
1083
|
+
metadata.terminalResult = entry;
|
|
1084
|
+
});
|
|
1082
1085
|
const finishActivity = metadata.rejectForUpdate
|
|
1083
1086
|
? () => undefined
|
|
1084
1087
|
: beginProxyRequest();
|
|
1085
1088
|
const finish = () => {
|
|
1089
|
+
stopObservingFinalLog();
|
|
1086
1090
|
finishActivity();
|
|
1087
1091
|
// Borrowed traffic holds a concurrency slot for the lifetime of the
|
|
1088
1092
|
// response body, so it is released here rather than when the handler
|
|
@@ -1130,15 +1134,9 @@ function registerProxyRequestTracking(app, requestMetadata, readiness) {
|
|
|
1130
1134
|
}
|
|
1131
1135
|
}
|
|
1132
1136
|
};
|
|
1133
|
-
const notifyRouteTerminal = (details) => {
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
observer.onTerminal?.(details);
|
|
1137
|
-
}
|
|
1138
|
-
catch {
|
|
1139
|
-
// Route-level accounting must never interfere with the relay.
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1137
|
+
const notifyRouteTerminal = async (details) => {
|
|
1138
|
+
const results = await withTimeout(Promise.allSettled(routeResponseObservers.map(async (observer) => observer.onTerminal?.(details))), 2_000, "Timed out joining proxy response accounting");
|
|
1139
|
+
return results.some((result) => result.status === "rejected");
|
|
1142
1140
|
};
|
|
1143
1141
|
c.res = trackProxyResponse(c.res, finish, {
|
|
1144
1142
|
onFirstChunk: ({ observedBodyBytes, responseChunks }) => {
|
|
@@ -1162,13 +1160,39 @@ function registerProxyRequestTracking(app, requestMetadata, readiness) {
|
|
|
1162
1160
|
responseChunks,
|
|
1163
1161
|
});
|
|
1164
1162
|
},
|
|
1165
|
-
onTerminal: ({ outcome, observedBodyBytes, responseChunks }) => {
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1163
|
+
onTerminal: async ({ outcome, error, observedBodyBytes, responseChunks, }) => {
|
|
1164
|
+
const terminalMonotonicMs = performance.now();
|
|
1165
|
+
const terminalTimestampMs = Date.now();
|
|
1166
|
+
// Route accounting may await SSE parsing/cancellation. Join it before
|
|
1167
|
+
// publishing the semantic terminal record; transport EOF alone is not
|
|
1168
|
+
// evidence of a successful model response.
|
|
1169
|
+
let accountingTimedOut = false;
|
|
1170
|
+
let accountingFailed = false;
|
|
1171
|
+
try {
|
|
1172
|
+
accountingFailed = await notifyRouteTerminal({
|
|
1173
|
+
outcome,
|
|
1174
|
+
error,
|
|
1175
|
+
observedBodyBytes,
|
|
1176
|
+
responseChunks,
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
catch {
|
|
1180
|
+
accountingTimedOut = true;
|
|
1169
1181
|
}
|
|
1182
|
+
const final = metadata.terminalResult;
|
|
1183
|
+
const terminalOutcome = final?.terminalOutcome ??
|
|
1184
|
+
(outcome === "stream_error" ||
|
|
1185
|
+
metadata.terminalErrorType === "stream_error"
|
|
1186
|
+
? "stream_error"
|
|
1187
|
+
: outcome === "client_cancelled"
|
|
1188
|
+
? "client_cancelled"
|
|
1189
|
+
: responseStatus >= 400
|
|
1190
|
+
? "handler_error"
|
|
1191
|
+
: "unknown");
|
|
1170
1192
|
logProxyLifecycleEvent({
|
|
1171
1193
|
event: "request_terminal",
|
|
1194
|
+
timestampMs: terminalTimestampMs,
|
|
1195
|
+
monotonicMs: terminalMonotonicMs,
|
|
1172
1196
|
requestId: metadata.requestId,
|
|
1173
1197
|
method: metadata.method,
|
|
1174
1198
|
path: metadata.path,
|
|
@@ -1180,20 +1204,33 @@ function registerProxyRequestTracking(app, requestMetadata, readiness) {
|
|
|
1180
1204
|
responseStatus,
|
|
1181
1205
|
observedBodyBytes,
|
|
1182
1206
|
responseChunks,
|
|
1183
|
-
elapsedMs:
|
|
1184
|
-
terminalOutcome
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1207
|
+
elapsedMs: terminalMonotonicMs - startedMonotonicMs,
|
|
1208
|
+
terminalOutcome,
|
|
1209
|
+
finalStatus: final?.responseStatus,
|
|
1210
|
+
transportOutcome: outcome,
|
|
1211
|
+
outcomeSource: final
|
|
1212
|
+
? "final_request"
|
|
1213
|
+
: responseStatus >= 400
|
|
1214
|
+
? "http_status"
|
|
1215
|
+
: terminalOutcome === "unknown"
|
|
1216
|
+
? "unknown"
|
|
1217
|
+
: "transport_error",
|
|
1218
|
+
telemetryStatus: accountingTimedOut
|
|
1219
|
+
? "timeout"
|
|
1220
|
+
: accountingFailed
|
|
1221
|
+
? "observer_error"
|
|
1222
|
+
: final
|
|
1223
|
+
? "complete"
|
|
1224
|
+
: "missing_final",
|
|
1225
|
+
errorType: final?.errorType ?? metadata.terminalErrorType,
|
|
1226
|
+
errorCode: final?.errorCode ?? metadata.terminalErrorCode,
|
|
1192
1227
|
});
|
|
1228
|
+
stopObservingFinalLog();
|
|
1193
1229
|
},
|
|
1194
1230
|
});
|
|
1195
1231
|
}
|
|
1196
1232
|
catch (error) {
|
|
1233
|
+
stopObservingFinalLog();
|
|
1197
1234
|
// Keep metadata available to app.onError, which records the client-facing
|
|
1198
1235
|
// failure with the same request ID before deleting the WeakMap entry.
|
|
1199
1236
|
finishActivity();
|
|
@@ -1238,7 +1275,7 @@ export async function createProxyStartApp(params) {
|
|
|
1238
1275
|
const { createOpenAIProxyRoutes } = await import("../../server/routes/openaiProxyRoutes.js");
|
|
1239
1276
|
const { createCodexProxyRoutes } = await import("../../server/routes/codexProxyRoutes.js");
|
|
1240
1277
|
const { createGeminiProxyRoutes } = await import("../../server/routes/geminiProxyRoutes.js");
|
|
1241
|
-
const { logBodyCapture, logRequest } = await import("../../proxy/requestLogger.js");
|
|
1278
|
+
const { logBodyCapture, logRequest, getRequestLoggerSnapshot } = await import("../../proxy/requestLogger.js");
|
|
1242
1279
|
const { recordFinalError } = await import("../../proxy/usageStats.js");
|
|
1243
1280
|
const { admitInboundShareRequest, isGrantRequiredByEnv } = await import("../../proxy/shareGate.js");
|
|
1244
1281
|
const { runWithShareContext } = await import("../../proxy/shareContext.js");
|
|
@@ -1978,6 +2015,7 @@ export async function createProxyStartApp(params) {
|
|
|
1978
2015
|
})(),
|
|
1979
2016
|
observability: {
|
|
1980
2017
|
lifecycle: getProxyLifecycleLoggerSnapshot(),
|
|
2018
|
+
requestLogs: getRequestLoggerSnapshot(),
|
|
1981
2019
|
},
|
|
1982
2020
|
autoUpdate: {
|
|
1983
2021
|
enabled: isProxyAutoUpdateEnabled(),
|
|
@@ -70,6 +70,7 @@ function printAnalysis(report) {
|
|
|
70
70
|
for (const [label, summary] of [
|
|
71
71
|
["Response headers", report.latencyMs.headers],
|
|
72
72
|
["First chunk", report.latencyMs.firstChunk],
|
|
73
|
+
["First useful output", report.latencyMs.firstUsefulOutput],
|
|
73
74
|
["Terminal", report.latencyMs.terminal],
|
|
74
75
|
["Final request log", report.latencyMs.finalRequest],
|
|
75
76
|
["Account attempt", report.latencyMs.attempt],
|
|
@@ -100,7 +101,9 @@ function printAnalysis(report) {
|
|
|
100
101
|
if (!report.coverage.comparableRequestAttempts) {
|
|
101
102
|
logger.always(chalk.yellow(" WARNING: request and attempt totals do not cover a comparable full window; do not reconcile them as one cohort"));
|
|
102
103
|
}
|
|
103
|
-
logger.always(` ${report.dataQuality.linesRead} lines scanned, ${report.dataQuality.malformedLines} malformed, ${report.dataQuality.unsupportedLifecycleLines} unsupported lifecycle, ${report.dataQuality.lifecycleSequenceGaps} sequence gaps, ${report.dataQuality.lifecycleSequenceDuplicates} duplicates`);
|
|
104
|
+
logger.always(` ${report.dataQuality.linesRead} lines scanned, ${report.dataQuality.malformedLines} malformed, ${report.dataQuality.unsupportedLifecycleLines} unsupported lifecycle, ${report.dataQuality.lifecycleSequenceGaps} sequence gaps, ${report.dataQuality.lifecycleSequenceDuplicates} duplicates (${report.dataQuality.conflictingLifecycleDuplicates} conflicting)`);
|
|
105
|
+
logger.always(` Outcome evidence: ${report.dataQuality.finalOutcomeConflicts} conflicts reconciled, ${report.dataQuality.acceptedWithoutFinal} accepted without a final record, ${report.dataQuality.terminalWithoutFinal} transport terminals without a final record`);
|
|
106
|
+
logger.always(` Repeated attempt records merged: ${report.dataQuality.duplicateAttempts}`);
|
|
104
107
|
logger.always(` Routing decisions: ${report.dataQuality.routingDecisions.valid} valid, ${report.dataQuality.routingDecisions.invalid} invalid, ${report.dataQuality.routingDecisions.absent} absent`);
|
|
105
108
|
for (const [stream, range] of Object.entries(report.dataQuality.streams)) {
|
|
106
109
|
if (range.observedFrom) {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* params, timing, and error status. Memory is bounded: serialized results are
|
|
10
10
|
* capped at `maxResultChars` and the record list at `maxRecords`.
|
|
11
11
|
*/
|
|
12
|
-
import type { Tool, ToolExecutionCaptureOptions, ToolExecutionRecord } from "../types/index.js";
|
|
12
|
+
import type { Tool, ToolExecutionCaptureOptions, ToolExecutionRecord, StandardRecord, ToolExecutionSummaryInternal } from "../types/index.js";
|
|
13
13
|
/** Default cap on serialized result characters kept per record (~8KB). */
|
|
14
14
|
export declare const DEFAULT_TOOL_RESULT_CAPTURE_CHARS = 8192;
|
|
15
15
|
/** Default cap on records kept per turn. */
|
|
@@ -73,4 +73,19 @@ export declare function toToolExecutionRecords(legacyExecutions: unknown[] | und
|
|
|
73
73
|
* carried a recorder that saw executions, else a conversion of the loop's
|
|
74
74
|
* legacy accumulator entries.
|
|
75
75
|
*/
|
|
76
|
+
/**
|
|
77
|
+
* The public `toolCalls` view of a native turn's execution summaries.
|
|
78
|
+
*
|
|
79
|
+
* The native generate loops record every executed call in
|
|
80
|
+
* `ToolExecutionSummaryInternal[]` and surface it as `toolExecutions`, but
|
|
81
|
+
* never mapped it onto `EnhancedGenerateResult.toolCalls` — the field the type
|
|
82
|
+
* has always declared and the ai-package formatter used to fill. A caller
|
|
83
|
+
* reading `result.toolCalls` after a tool ran saw nothing. This is the one
|
|
84
|
+
* place that mapping lives, so the three native paths cannot drift apart.
|
|
85
|
+
*/
|
|
86
|
+
export declare function toolCallsFromSummaries(summaries: ReadonlyArray<ToolExecutionSummaryInternal>): Array<{
|
|
87
|
+
toolCallId: string;
|
|
88
|
+
toolName: string;
|
|
89
|
+
args: StandardRecord;
|
|
90
|
+
}>;
|
|
76
91
|
export declare function resolveToolExecutionRecords(options: unknown, legacyExecutions?: unknown[]): ToolExecutionRecord[];
|
|
@@ -247,6 +247,27 @@ export function toToolExecutionRecords(legacyExecutions, capture) {
|
|
|
247
247
|
* carried a recorder that saw executions, else a conversion of the loop's
|
|
248
248
|
* legacy accumulator entries.
|
|
249
249
|
*/
|
|
250
|
+
/**
|
|
251
|
+
* The public `toolCalls` view of a native turn's execution summaries.
|
|
252
|
+
*
|
|
253
|
+
* The native generate loops record every executed call in
|
|
254
|
+
* `ToolExecutionSummaryInternal[]` and surface it as `toolExecutions`, but
|
|
255
|
+
* never mapped it onto `EnhancedGenerateResult.toolCalls` — the field the type
|
|
256
|
+
* has always declared and the ai-package formatter used to fill. A caller
|
|
257
|
+
* reading `result.toolCalls` after a tool ran saw nothing. This is the one
|
|
258
|
+
* place that mapping lives, so the three native paths cannot drift apart.
|
|
259
|
+
*/
|
|
260
|
+
export function toolCallsFromSummaries(summaries) {
|
|
261
|
+
return summaries.map((summary) => ({
|
|
262
|
+
toolCallId: summary.toolCallId,
|
|
263
|
+
toolName: summary.toolName,
|
|
264
|
+
args: summary.input !== null &&
|
|
265
|
+
typeof summary.input === "object" &&
|
|
266
|
+
!Array.isArray(summary.input)
|
|
267
|
+
? summary.input
|
|
268
|
+
: {},
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
250
271
|
export function resolveToolExecutionRecords(options, legacyExecutions) {
|
|
251
272
|
const recorder = ToolExecutionRecorder.from(options);
|
|
252
273
|
if (recorder?.hasRecords()) {
|
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
import { createBlockedResponse, createBlockedStream, applyContentFiltering, handlePrecallGuardrails, } from "../utils/guardrailsUtils.js";
|
|
2
2
|
import { logger } from "../../utils/logger.js";
|
|
3
3
|
import { generateOnceNative } from "../../utils/nativeSingleShot.js";
|
|
4
|
+
/**
|
|
5
|
+
* Filter each contiguous run of text parts as one string.
|
|
6
|
+
*
|
|
7
|
+
* A prohibited term that straddles two adjacent text parts is invisible to a
|
|
8
|
+
* per-part filter, and the parts are concatenated downstream (`lifecycle.ts`
|
|
9
|
+
* joins adjacent text), so the term reached the caller whole. Anthropic's
|
|
10
|
+
* native path emits one text part per content block, so adjacent parts are a
|
|
11
|
+
* real shape, not a theoretical one. Non-text parts keep their position; a run
|
|
12
|
+
* is rebuilt as a single text part only when the filter changed it.
|
|
13
|
+
*/
|
|
14
|
+
const filterTextRuns = (content, badWords, context) => {
|
|
15
|
+
const out = [];
|
|
16
|
+
let run = [];
|
|
17
|
+
const flushRun = () => {
|
|
18
|
+
if (run.length === 0) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const merged = run.map((part) => part.text).join("");
|
|
22
|
+
const filtered = applyContentFiltering(merged, badWords, context);
|
|
23
|
+
if (filtered.hasChanges) {
|
|
24
|
+
out.push({ ...run[0], text: filtered.filteredText });
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
out.push(...run);
|
|
28
|
+
}
|
|
29
|
+
run = [];
|
|
30
|
+
};
|
|
31
|
+
for (const part of content) {
|
|
32
|
+
if (part.type === "text") {
|
|
33
|
+
run.push(part);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
flushRun();
|
|
37
|
+
out.push(part);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
flushRun();
|
|
41
|
+
return out;
|
|
42
|
+
};
|
|
4
43
|
/**
|
|
5
44
|
* Create Guardrails AI middleware for content filtering and policy enforcement
|
|
6
45
|
* @param config Configuration for the guardrails middleware
|
|
@@ -61,12 +100,7 @@ export function createGuardrailsMiddleware(config = {}) {
|
|
|
61
100
|
let result = await doGenerate();
|
|
62
101
|
result = {
|
|
63
102
|
...result,
|
|
64
|
-
content: result.content
|
|
65
|
-
? {
|
|
66
|
-
...part,
|
|
67
|
-
text: applyContentFiltering(part.text, config.badWords, "generate").filteredText,
|
|
68
|
-
}
|
|
69
|
-
: part),
|
|
103
|
+
content: filterTextRuns(result.content, config.badWords, "generate"),
|
|
70
104
|
};
|
|
71
105
|
if (config.modelFilter?.enabled && config.modelFilter.filterModel) {
|
|
72
106
|
logger.debug(`[GuardrailsMiddleware] Invoking model-based filter.`);
|
|
@@ -113,22 +147,38 @@ export function createGuardrailsMiddleware(config = {}) {
|
|
|
113
147
|
}
|
|
114
148
|
const { stream, ...rest } = await doStream();
|
|
115
149
|
let hasYieldedChunks = false;
|
|
150
|
+
// With bad-word filtering on, a text run is buffered and filtered as one
|
|
151
|
+
// string: a term split across deltas is invisible per delta and every
|
|
152
|
+
// consumer reassembles it. The run is released when a non-text part
|
|
153
|
+
// arrives or the stream ends, so the guardrail trades incremental
|
|
154
|
+
// delivery of that run for not being bypassable by chunking. With
|
|
155
|
+
// filtering off, deltas pass through untouched and unbuffered.
|
|
156
|
+
const bufferTextRuns = config.badWords?.enabled === true;
|
|
157
|
+
let pendingText;
|
|
158
|
+
const releaseText = (controller) => {
|
|
159
|
+
if (!pendingText) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const filtered = applyContentFiltering(pendingText.delta, config.badWords, "stream");
|
|
163
|
+
controller.enqueue(filtered.hasChanges
|
|
164
|
+
? { ...pendingText, delta: filtered.filteredText }
|
|
165
|
+
: pendingText);
|
|
166
|
+
pendingText = undefined;
|
|
167
|
+
};
|
|
116
168
|
const transformStream = new TransformStream({
|
|
117
169
|
transform(chunk, controller) {
|
|
118
170
|
hasYieldedChunks = true;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
...filteredChunk,
|
|
125
|
-
delta: filterResult.filteredText,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
171
|
+
if (chunk.type === "text-delta" && bufferTextRuns) {
|
|
172
|
+
pendingText = pendingText
|
|
173
|
+
? { ...pendingText, delta: pendingText.delta + chunk.delta }
|
|
174
|
+
: chunk;
|
|
175
|
+
return;
|
|
128
176
|
}
|
|
129
|
-
controller
|
|
177
|
+
releaseText(controller);
|
|
178
|
+
controller.enqueue(chunk);
|
|
130
179
|
},
|
|
131
|
-
flush() {
|
|
180
|
+
flush(controller) {
|
|
181
|
+
releaseText(controller);
|
|
132
182
|
if (!hasYieldedChunks) {
|
|
133
183
|
logger.warn(`[GuardrailsMiddleware] Stream ended without yielding any chunks`);
|
|
134
184
|
}
|
package/dist/neurolink.js
CHANGED
|
@@ -4359,6 +4359,7 @@ Current user's request: ${currentInput}`;
|
|
|
4359
4359
|
: undefined,
|
|
4360
4360
|
responseTime: textResult.responseTime,
|
|
4361
4361
|
toolsUsed: textResult.toolsUsed,
|
|
4362
|
+
toolCalls: textResult.toolCalls ?? [],
|
|
4362
4363
|
toolExecutions: toToolExecutionRecords(textResult.toolExecutions),
|
|
4363
4364
|
enhancedWithTools: textResult.enhancedWithTools,
|
|
4364
4365
|
availableTools: transformAvailableTools(textResult.availableTools),
|
|
@@ -5877,6 +5878,7 @@ Current user's request: ${currentInput}`;
|
|
|
5877
5878
|
rawFinishReason: result.rawFinishReason,
|
|
5878
5879
|
stepsUsed: result.stepsUsed,
|
|
5879
5880
|
toolsUsed: result.toolsUsed || [],
|
|
5881
|
+
toolCalls: result.toolCalls ?? [],
|
|
5880
5882
|
toolExecutions: transformedToolExecutions,
|
|
5881
5883
|
enhancedWithTools: Boolean(hasToolExecutions),
|
|
5882
5884
|
availableTools: transformToolsForMCP(transformToolsToExpectedFormat(availableTools)),
|
|
@@ -6028,6 +6030,7 @@ Current user's request: ${currentInput}`;
|
|
|
6028
6030
|
rawFinishReason: poolResult.rawFinishReason,
|
|
6029
6031
|
stepsUsed: poolResult.stepsUsed,
|
|
6030
6032
|
toolsUsed: poolResult.toolsUsed || [],
|
|
6033
|
+
toolCalls: poolResult.toolCalls ?? [],
|
|
6031
6034
|
// Lossless pass-through: keep the full ToolExecutionRecord
|
|
6032
6035
|
// fields (params/resultText/isError/timing) alongside the
|
|
6033
6036
|
// legacy {toolName,executionTime,success} shape this internal
|
|
@@ -6398,6 +6401,9 @@ Current user's request: ${currentInput}`;
|
|
|
6398
6401
|
rawFinishReason: result.rawFinishReason,
|
|
6399
6402
|
stepsUsed: result.stepsUsed,
|
|
6400
6403
|
toolsUsed: result.toolsUsed || [],
|
|
6404
|
+
// The providers record executed calls; without this line the public
|
|
6405
|
+
// result never carried them, whatever the provider returned.
|
|
6406
|
+
toolCalls: result.toolCalls ?? [],
|
|
6401
6407
|
// Lossless pass-through: keep the full ToolExecutionRecord fields
|
|
6402
6408
|
// alongside the legacy {toolName,executionTime,success} shape this
|
|
6403
6409
|
// internal result declares, so the final GenerateResult mapping
|
|
@@ -2,7 +2,7 @@ import { BaseProvider } from "../core/baseProvider.js";
|
|
|
2
2
|
import { createStreamChannel } from "../core/streamChannel.js";
|
|
3
3
|
import { logger } from "../utils/logger.js";
|
|
4
4
|
import { resolveRequestKind } from "../core/resolveRequestKind.js";
|
|
5
|
-
import { resolveToolExecutionRecords } from "../core/toolExecutionRecorder.js";
|
|
5
|
+
import { resolveToolExecutionRecords, toolCallsFromSummaries, } from "../core/toolExecutionRecorder.js";
|
|
6
6
|
import { transformToolExecutions } from "../utils/transformationUtils.js";
|
|
7
7
|
import { convertZodToJsonSchema } from "../utils/schemaConversion.js";
|
|
8
8
|
import { withProviderRetry } from "../utils/providerRetry.js";
|
|
@@ -198,6 +198,7 @@ export class AmazonSageMakerProvider extends BaseProvider {
|
|
|
198
198
|
},
|
|
199
199
|
responseTime: Date.now() - startTime,
|
|
200
200
|
toolsUsed: loop.toolsUsed,
|
|
201
|
+
toolCalls: toolCallsFromSummaries(toolExecutionSummaries),
|
|
201
202
|
toolExecutions: resolveToolExecutionRecords(options, transformToolExecutions(toolExecutionSummaries)),
|
|
202
203
|
enhancedWithTools: loop.toolsUsed.length > 0,
|
|
203
204
|
};
|
|
@@ -28,7 +28,7 @@ import { runAgenticLoop } from "../../core/loopEngine.js";
|
|
|
28
28
|
import { hasNativeDoGenerate, runNativeGenerateLoop, } from "../../core/nativeGenerateLoop.js";
|
|
29
29
|
import { withProviderRetry } from "../../utils/providerRetry.js";
|
|
30
30
|
import { resolveRequestKind } from "../../core/resolveRequestKind.js";
|
|
31
|
-
import { resolveToolExecutionRecords } from "../../core/toolExecutionRecorder.js";
|
|
31
|
+
import { resolveToolExecutionRecords, toolCallsFromSummaries, } from "../../core/toolExecutionRecorder.js";
|
|
32
32
|
import { transformToolExecutions } from "../../utils/transformationUtils.js";
|
|
33
33
|
import { createAnthropicConfig, getProviderModel, validateApiKey, } from "../../utils/providerConfig.js";
|
|
34
34
|
import { composeAbortSignals, createTimeoutController, mergeAbortSignals, TimeoutError, } from "../../utils/timeout.js";
|
|
@@ -1479,6 +1479,7 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
1479
1479
|
},
|
|
1480
1480
|
responseTime: Date.now() - startTime,
|
|
1481
1481
|
toolsUsed: loop.toolsUsed,
|
|
1482
|
+
toolCalls: toolCallsFromSummaries(toolExecutionSummaries),
|
|
1482
1483
|
toolExecutions: resolveToolExecutionRecords(options, transformToolExecutions(toolExecutionSummaries)),
|
|
1483
1484
|
enhancedWithTools: loop.toolsUsed.length > 0,
|
|
1484
1485
|
};
|
|
@@ -34,7 +34,7 @@ import { composeAbortSignalsScoped, createTimeoutController, mergeAbortSignals,
|
|
|
34
34
|
import { emitToolEndFromStepFinish } from "../utils/toolEndEmitter.js";
|
|
35
35
|
import { resolveRequestKind } from "../core/resolveRequestKind.js";
|
|
36
36
|
import { appendJsonSchemaInstruction, hasNativeDoGenerate, runNativeGenerateLoop, } from "../core/nativeGenerateLoop.js";
|
|
37
|
-
import { resolveToolExecutionRecords } from "../core/toolExecutionRecorder.js";
|
|
37
|
+
import { resolveToolExecutionRecords, toolCallsFromSummaries, } from "../core/toolExecutionRecorder.js";
|
|
38
38
|
import { convertZodToJsonSchema } from "../utils/schemaConversion.js";
|
|
39
39
|
import { coerceJsonToSchema, schemaAccepts } from "../utils/json/coerce.js";
|
|
40
40
|
import { resolveToolChoice } from "../utils/toolChoice.js";
|
|
@@ -989,6 +989,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
|
|
|
989
989
|
},
|
|
990
990
|
responseTime: Date.now() - startTime,
|
|
991
991
|
toolsUsed,
|
|
992
|
+
toolCalls: toolCallsFromSummaries(toolExecutionSummaries),
|
|
992
993
|
toolExecutions: resolveToolExecutionRecords(options, transformToolExecutions(toolExecutionSummaries)),
|
|
993
994
|
enhancedWithTools: toolsUsed.length > 0,
|
|
994
995
|
};
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* `prompt_tokens`/`completion_tokens` spellings. A `null` result means "not
|
|
40
40
|
* observed", never "zero tokens".
|
|
41
41
|
*/
|
|
42
|
-
import type { CodexStreamUsage } from "../types/index.js";
|
|
42
|
+
import type { CodexStreamUsage, CodexStreamEvidence } from "../types/index.js";
|
|
43
43
|
/**
|
|
44
44
|
* Pull usage out of one parsed SSE `data:` payload.
|
|
45
45
|
*
|
|
@@ -65,4 +65,5 @@ export declare function scanCodexSSEForUsage(text: string): CodexStreamUsage | n
|
|
|
65
65
|
export declare function createCodexUsageTap(): {
|
|
66
66
|
stream: TransformStream<Uint8Array, Uint8Array>;
|
|
67
67
|
usage: Promise<CodexStreamUsage | null>;
|
|
68
|
+
evidence: () => CodexStreamEvidence;
|
|
68
69
|
};
|
package/dist/proxy/codexUsage.js
CHANGED
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
* observed", never "zero tokens".
|
|
41
41
|
*/
|
|
42
42
|
import { appendFileSync } from "node:fs";
|
|
43
|
+
import { extractSSEEvents } from "./sseInterceptor.js";
|
|
44
|
+
import { sanitizeForLog } from "../utils/logSanitize.js";
|
|
43
45
|
const nonNegativeInt = (value) => typeof value === "number" && Number.isFinite(value) && value > 0
|
|
44
46
|
? Math.floor(value)
|
|
45
47
|
: 0;
|
|
@@ -161,6 +163,68 @@ function createCaptureSink() {
|
|
|
161
163
|
* none was. It never rejects.
|
|
162
164
|
*/
|
|
163
165
|
export function createCodexUsageTap() {
|
|
166
|
+
const evidence = { completed: false, terminalBytes: 0 };
|
|
167
|
+
let totalBytes = 0;
|
|
168
|
+
const inspectEvidence = (events) => {
|
|
169
|
+
for (const frame of events) {
|
|
170
|
+
try {
|
|
171
|
+
const event = JSON.parse(frame.data);
|
|
172
|
+
if (!event || typeof event !== "object") {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const seen = extractCodexUsage(event);
|
|
176
|
+
if (seen) {
|
|
177
|
+
latest = seen;
|
|
178
|
+
}
|
|
179
|
+
const type = event.type ?? frame.event;
|
|
180
|
+
if ((type === "response.output_text.delta" ||
|
|
181
|
+
type === "response.function_call_arguments.delta") &&
|
|
182
|
+
typeof event.delta === "string" &&
|
|
183
|
+
event.delta.length > 0) {
|
|
184
|
+
evidence.firstUsefulOutputAt ??= Date.now();
|
|
185
|
+
}
|
|
186
|
+
if (type === "response.completed") {
|
|
187
|
+
evidence.completed = true;
|
|
188
|
+
evidence.terminalBytes = totalBytes;
|
|
189
|
+
}
|
|
190
|
+
else if (type === "error" ||
|
|
191
|
+
type === "response.failed" ||
|
|
192
|
+
type === "response.incomplete") {
|
|
193
|
+
evidence.errorType = "stream_error";
|
|
194
|
+
const response = event.response;
|
|
195
|
+
const details = response && typeof response === "object"
|
|
196
|
+
? response
|
|
197
|
+
: event;
|
|
198
|
+
const rawError = details.error;
|
|
199
|
+
const error = rawError && typeof rawError === "object"
|
|
200
|
+
? rawError
|
|
201
|
+
: details;
|
|
202
|
+
const incomplete = details.incomplete_details;
|
|
203
|
+
const reason = incomplete &&
|
|
204
|
+
typeof incomplete === "object" &&
|
|
205
|
+
"reason" in incomplete
|
|
206
|
+
? incomplete.reason
|
|
207
|
+
: undefined;
|
|
208
|
+
evidence.errorCode =
|
|
209
|
+
typeof error.code === "string"
|
|
210
|
+
? sanitizeForLog(error.code).slice(0, 200)
|
|
211
|
+
: typeof reason === "string"
|
|
212
|
+
? sanitizeForLog(reason).slice(0, 200)
|
|
213
|
+
: String(type);
|
|
214
|
+
evidence.errorMessage =
|
|
215
|
+
typeof error.message === "string"
|
|
216
|
+
? sanitizeForLog(error.message).slice(0, 200)
|
|
217
|
+
: type === "response.incomplete"
|
|
218
|
+
? "Codex reported an incomplete response"
|
|
219
|
+
: "Codex reported a stream failure";
|
|
220
|
+
evidence.terminalBytes = totalBytes;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// Unknown frames cannot establish successful completion.
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
164
228
|
let settleUsage = () => { };
|
|
165
229
|
const usage = new Promise((resolve) => {
|
|
166
230
|
settleUsage = resolve;
|
|
@@ -179,40 +243,33 @@ export function createCodexUsageTap() {
|
|
|
179
243
|
const capture = createCaptureSink();
|
|
180
244
|
let carry = "";
|
|
181
245
|
let latest = null;
|
|
182
|
-
|
|
183
|
-
* Ceiling on the unterminated tail we are willing to hold.
|
|
184
|
-
*
|
|
185
|
-
* `carry` normally holds a fraction of one SSE line, because every newline
|
|
186
|
-
* flushes it. A stream that never sends one — a hung upstream, a
|
|
187
|
-
* non-SSE body relayed by mistake — would otherwise grow it without bound
|
|
188
|
-
* for the life of the request. One `response.completed` event is a few
|
|
189
|
-
* hundred bytes, so a megabyte is far past any real event, and dropping the
|
|
190
|
-
* tail costs at most the usage reading this tap is allowed to miss anyway.
|
|
191
|
-
*/
|
|
246
|
+
// Bound malformed unterminated events without ever withholding relay bytes.
|
|
192
247
|
const CARRY_LIMIT_CHARS = 1024 * 1024;
|
|
248
|
+
let discardingEvent = false;
|
|
193
249
|
const transformer = {
|
|
194
250
|
transform(chunk, controller) {
|
|
195
251
|
// Bytes go out first and unconditionally: nothing below can delay or
|
|
196
252
|
// alter what the client receives.
|
|
197
253
|
controller.enqueue(chunk);
|
|
254
|
+
totalBytes += chunk.byteLength;
|
|
198
255
|
try {
|
|
199
256
|
capture?.(chunk);
|
|
200
257
|
carry += decoder.decode(chunk, { stream: true });
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
// read. Give up on the tail rather than grow forever.
|
|
207
|
-
carry = "";
|
|
258
|
+
if (discardingEvent) {
|
|
259
|
+
const boundary = /\r\n\r\n|\n\n|\r\r/.exec(carry);
|
|
260
|
+
if (!boundary) {
|
|
261
|
+
carry = carry.slice(-3);
|
|
262
|
+
return;
|
|
208
263
|
}
|
|
209
|
-
|
|
264
|
+
carry = carry.slice(boundary.index + boundary[0].length);
|
|
265
|
+
discardingEvent = false;
|
|
210
266
|
}
|
|
211
|
-
const
|
|
212
|
-
carry =
|
|
213
|
-
|
|
214
|
-
if (
|
|
215
|
-
|
|
267
|
+
const { events, remainder } = extractSSEEvents(carry);
|
|
268
|
+
carry = remainder;
|
|
269
|
+
inspectEvidence(events);
|
|
270
|
+
if (carry.length > CARRY_LIMIT_CHARS) {
|
|
271
|
+
carry = carry.slice(-2);
|
|
272
|
+
discardingEvent = true;
|
|
216
273
|
}
|
|
217
274
|
}
|
|
218
275
|
catch {
|
|
@@ -220,15 +277,7 @@ export function createCodexUsageTap() {
|
|
|
220
277
|
}
|
|
221
278
|
},
|
|
222
279
|
flush() {
|
|
223
|
-
|
|
224
|
-
const seen = scanCodexSSEForUsage(carry);
|
|
225
|
-
if (seen) {
|
|
226
|
-
latest = seen;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
catch {
|
|
230
|
-
// ignored — see above
|
|
231
|
-
}
|
|
280
|
+
// An event without its dispatch delimiter is incomplete on the wire.
|
|
232
281
|
settle(latest);
|
|
233
282
|
},
|
|
234
283
|
/**
|
|
@@ -242,5 +291,5 @@ export function createCodexUsageTap() {
|
|
|
242
291
|
},
|
|
243
292
|
};
|
|
244
293
|
const stream = new TransformStream(transformer);
|
|
245
|
-
return { stream, usage };
|
|
294
|
+
return { stream, usage, evidence: () => ({ ...evidence }) };
|
|
246
295
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type { ProxyActivitySnapshot, ProxyResponseTrackingObserver } from "../types/index.js";
|
|
1
|
+
import type { ProxyActivitySnapshot, ProxyResponseTrackingObserver, RequestLogEntry } from "../types/index.js";
|
|
2
|
+
/** Join route accounting to the HTTP lifecycle without relying on write order. */
|
|
3
|
+
export declare function observeProxyFinalLog(requestId: string, observer: (entry: RequestLogEntry) => void): () => void;
|
|
4
|
+
export declare function notifyProxyFinalLog(entry: RequestLogEntry): void;
|
|
2
5
|
export declare function registerProxyResponseObserver(metadata: object, observer: ProxyResponseTrackingObserver): void;
|
|
3
6
|
export declare function takeProxyResponseObservers(metadata: object): ProxyResponseTrackingObserver[];
|
|
4
7
|
/** Track one client-facing proxy request until its response body settles. */
|