mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-31 14:47:13 +00:00
147 lines
6.6 KiB
JavaScript
Executable File
147 lines
6.6 KiB
JavaScript
Executable File
export function summarizeFakeProviderState(state) {
|
|
if (!state) return null;
|
|
const recentRequests = Array.isArray(state.recent_requests) ? state.recent_requests : [];
|
|
const chatRequests = recentRequests.filter((request) => String(request?.path || "").includes("/chat/completions"));
|
|
const successfulRequests = chatRequests.filter((request) => request?.status === "ok");
|
|
const faultRequests = chatRequests.filter((request) => (
|
|
request?.should_fail === true
|
|
|| request?.status === "http_fault"
|
|
|| request?.status === "mid_stream_disconnect"
|
|
|| request?.status === "mid_stream_error_event"
|
|
|| (Number.isFinite(request?.http_status) && request.http_status >= 400)
|
|
));
|
|
const modelRequestCounts = {};
|
|
for (const request of chatRequests) {
|
|
const model = String(request?.model || "");
|
|
if (!model) continue;
|
|
modelRequestCounts[model] = (modelRequestCounts[model] || 0) + 1;
|
|
}
|
|
|
|
return {
|
|
status: state.status || "unknown",
|
|
url: state.url || "",
|
|
request_count: Number.isFinite(state.request_count) ? state.request_count : recentRequests.length,
|
|
recent_request_count: recentRequests.length,
|
|
chat_request_count: chatRequests.length,
|
|
fault_count: faultRequests.length,
|
|
mid_stream_disconnect_count: chatRequests.filter(
|
|
(request) => ["mid_stream_disconnect", "mid_stream_error_event"].includes(request?.status),
|
|
).length,
|
|
model_request_counts: modelRequestCounts,
|
|
streamed_request_count: chatRequests.filter((request) => request?.stream === true).length,
|
|
duration_ms: stats(chatRequests.map((request) => numberOrNull(request?.duration_ms)).filter(Number.isFinite)),
|
|
successful_duration_ms: stats(successfulRequests.map((request) => numberOrNull(request?.duration_ms)).filter(Number.isFinite)),
|
|
first_chunk_ms: stats(successfulRequests.map((request) => numberOrNull(request?.first_chunk_ms)).filter(Number.isFinite)),
|
|
first_content_chunk_ms: stats(successfulRequests.map((request) => numberOrNull(request?.first_content_chunk_ms)).filter(Number.isFinite)),
|
|
content_chunk_count: stats(successfulRequests.map((request) => numberOrNull(request?.content_chunk_count)).filter(Number.isFinite)),
|
|
config: state.config || {},
|
|
};
|
|
}
|
|
|
|
export function buildProviderTimingMetrics(samples, state) {
|
|
const recentRequests = Array.isArray(state?.recent_requests) ? state.recent_requests : [];
|
|
const byExpectedText = new Map();
|
|
for (const request of recentRequests) {
|
|
const expected = String(request?.expected_text || "");
|
|
if (!expected) continue;
|
|
if (!byExpectedText.has(expected)) byExpectedText.set(expected, []);
|
|
byExpectedText.get(expected).push(request);
|
|
}
|
|
|
|
const segments = [];
|
|
const missingExpectedText = [];
|
|
for (const sample of samples) {
|
|
const expected = String(sample?.expected_text || "");
|
|
if (!expected) continue;
|
|
const request = (byExpectedText.get(expected) || []).shift();
|
|
if (!request) {
|
|
missingExpectedText.push(expected);
|
|
continue;
|
|
}
|
|
const segment = buildTimingSegment(sample, request);
|
|
if (segment) segments.push(segment);
|
|
}
|
|
|
|
const values = (key) => segments.map((segment) => numberOrNull(segment[key])).filter(Number.isFinite);
|
|
return {
|
|
matched_request_count: segments.length,
|
|
missing_provider_match_count: missingExpectedText.length,
|
|
missing_expected_text: missingExpectedText.slice(0, 20),
|
|
send_to_provider_start_ms: stats(values("send_to_provider_start_ms")),
|
|
provider_duration_ms: stats(values("provider_duration_ms")),
|
|
provider_finish_to_ws_final_ms: stats(values("provider_finish_to_ws_final_ms")),
|
|
langbot_overhead_estimate_ms: stats(values("langbot_overhead_estimate_ms")),
|
|
e2e_minus_provider_ms: stats(values("e2e_minus_provider_ms")),
|
|
provider_first_content_to_ws_first_content_ms: stats(values("provider_first_content_to_ws_first_content_ms")),
|
|
segments,
|
|
};
|
|
}
|
|
|
|
function buildTimingSegment(sample, request) {
|
|
const sentEpochMs = numberOrNull(sample.sent_epoch_ms);
|
|
const finishedEpochMs = numberOrNull(sample.finished_epoch_ms);
|
|
const providerStartedEpochMs = numberOrNull(request.started_epoch_ms);
|
|
const providerFinishedEpochMs = numberOrNull(request.finished_epoch_ms);
|
|
const providerFirstContentEpochMs = numberOrNull(request.first_content_chunk_epoch_ms);
|
|
const wsFirstContentEpochMs = numberOrNull(sample.first_assistant_content_epoch_ms);
|
|
const responseDurationMs = numberOrNull(sample.response_duration_ms);
|
|
const providerDurationMs = numberOrNull(request.duration_ms);
|
|
|
|
const sendToProviderStartMs = finiteDelta(providerStartedEpochMs, sentEpochMs);
|
|
const providerFinishToWsFinalMs = finiteDelta(finishedEpochMs, providerFinishedEpochMs);
|
|
const e2eMinusProviderMs = Number.isFinite(responseDurationMs) && Number.isFinite(providerDurationMs)
|
|
? rounded(responseDurationMs - providerDurationMs)
|
|
: null;
|
|
const overheadEstimateMs = Number.isFinite(sendToProviderStartMs) && Number.isFinite(providerFinishToWsFinalMs)
|
|
? rounded(sendToProviderStartMs + providerFinishToWsFinalMs)
|
|
: e2eMinusProviderMs;
|
|
|
|
return {
|
|
sample_index: sample.index,
|
|
pipeline_label: sample.pipeline_label || "",
|
|
expected_text: sample.expected_text || "",
|
|
provider_request_id: request.id || "",
|
|
provider_request_number: request.request_number ?? null,
|
|
response_duration_ms: responseDurationMs,
|
|
provider_duration_ms: providerDurationMs,
|
|
send_to_provider_start_ms: sendToProviderStartMs,
|
|
provider_finish_to_ws_final_ms: providerFinishToWsFinalMs,
|
|
langbot_overhead_estimate_ms: overheadEstimateMs,
|
|
e2e_minus_provider_ms: e2eMinusProviderMs,
|
|
provider_first_content_to_ws_first_content_ms: finiteDelta(wsFirstContentEpochMs, providerFirstContentEpochMs),
|
|
provider_status: request.status || "",
|
|
provider_http_status: request.http_status ?? null,
|
|
};
|
|
}
|
|
|
|
function finiteDelta(left, right) {
|
|
return Number.isFinite(left) && Number.isFinite(right) ? rounded(left - right) : null;
|
|
}
|
|
|
|
export function stats(values) {
|
|
if (values.length === 0) return { min: 0, p50: 0, p95: 0, p99: 0, max: 0 };
|
|
return {
|
|
min: rounded(Math.min(...values)),
|
|
p50: percentile(values, 50),
|
|
p95: percentile(values, 95),
|
|
p99: percentile(values, 99),
|
|
max: rounded(Math.max(...values)),
|
|
};
|
|
}
|
|
|
|
export function percentile(values, percentileValue) {
|
|
if (values.length === 0) return 0;
|
|
const sorted = [...values].sort((a, b) => a - b);
|
|
const index = Math.min(sorted.length - 1, Math.ceil((percentileValue / 100) * sorted.length) - 1);
|
|
return rounded(sorted[index]);
|
|
}
|
|
|
|
export function rounded(value) {
|
|
return Number(value.toFixed(3));
|
|
}
|
|
|
|
function numberOrNull(value) {
|
|
const number = Number(value);
|
|
return Number.isFinite(number) ? number : null;
|
|
}
|