mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 06:42:12 +03:00
chatCore god-file decomposition (#3501): extract 6 pure leaves (cacheUsageMeta, executorClientHeaders, nonStreamingResponseBody, skillsFormat, streamErrorResult, streamFinalize) from chatCore.ts. Rebased onto release/v3.8.35 tip (resolved single chatCore.ts conflict — removed now-extracted inline buildExecutorClientHeaders). 265/265 chatcore tests, 26/26 new leaf tests, typecheck:core, cycles, file-size all green. Integrated into release/v3.8.35.
This commit is contained in:
committed by
GitHub
parent
be78e05925
commit
619c2eaaeb
@@ -126,6 +126,16 @@ import { updateProviderConnection, getProviderConnectionById } from "@/lib/db/pr
|
||||
import { wasRefreshTokenRotated } from "@omniroute/open-sse/services/refreshSerializer.ts";
|
||||
import { connectionHasExtraKeys } from "../services/apiKeyRotator.ts";
|
||||
import { recordKeyHealthStatus as recordKeyHealthStatusFor } from "./chatCore/keyHealth.ts";
|
||||
import { getSkillsModelIdForFormat } from "./chatCore/skillsFormat.ts";
|
||||
import { readNonStreamingResponseBody } from "./chatCore/nonStreamingResponseBody.ts";
|
||||
import {
|
||||
isSemaphoreCapacityError,
|
||||
createStreamingErrorResult,
|
||||
getUpstreamErrorIdentifier,
|
||||
} from "./chatCore/streamErrorResult.ts";
|
||||
import { wrapReadableStreamWithFinalize } from "./chatCore/streamFinalize.ts";
|
||||
import { buildCacheUsageLogMeta, attachLogMeta } from "./chatCore/cacheUsageMeta.ts";
|
||||
import { buildExecutorClientHeaders } from "./chatCore/executorClientHeaders.ts";
|
||||
|
||||
import {
|
||||
getCallLogPipelineCaptureStreamChunks,
|
||||
@@ -278,222 +288,6 @@ import { incrementRequestCount } from "../services/geminiRateLimitTracker.ts";
|
||||
|
||||
import { isSmallEnoughForSemanticCache } from "../utils/estimateSize.ts";
|
||||
|
||||
function getSkillsProviderForFormat(format: string): "openai" | "anthropic" | "google" | "other" {
|
||||
switch (format) {
|
||||
case FORMATS.CLAUDE:
|
||||
return "anthropic";
|
||||
case FORMATS.GEMINI:
|
||||
return "google";
|
||||
default:
|
||||
return "openai";
|
||||
}
|
||||
}
|
||||
|
||||
function getSkillsModelIdForFormat(format: string): string {
|
||||
switch (format) {
|
||||
case FORMATS.CLAUDE:
|
||||
return "claude";
|
||||
case FORMATS.GEMINI:
|
||||
return "gemini";
|
||||
default:
|
||||
return "openai";
|
||||
}
|
||||
}
|
||||
|
||||
async function readNonStreamingResponseBody(
|
||||
response: Response,
|
||||
contentType: string,
|
||||
upstreamStream: boolean
|
||||
): Promise<string> {
|
||||
if (
|
||||
!upstreamStream ||
|
||||
!response.body ||
|
||||
(!contentType.includes("text/event-stream") && !contentType.includes("application/x-ndjson"))
|
||||
) {
|
||||
return withBodyTimeout<string>(response.text());
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
const terminalState: NonStreamingSseTerminalState = {
|
||||
currentEvent: "",
|
||||
pendingLine: "",
|
||||
};
|
||||
let rawBody = "";
|
||||
const deadline = FETCH_BODY_TIMEOUT_MS > 0 ? Date.now() + FETCH_BODY_TIMEOUT_MS : 0;
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const timeoutMs = deadline > 0 ? deadline - Date.now() : 0;
|
||||
if (deadline > 0 && timeoutMs <= 0) {
|
||||
throw createBodyTimeoutError(FETCH_BODY_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
const { done, value } = await readStreamChunkWithTimeout(reader, timeoutMs);
|
||||
if (done) break;
|
||||
if (!value) continue;
|
||||
|
||||
const decodedChunk = decoder.decode(value, { stream: true });
|
||||
rawBody += decodedChunk;
|
||||
if (appendNonStreamingSseTerminalSignal(terminalState, decodedChunk)) {
|
||||
await reader.cancel("non-streaming bridge consumed terminal SSE event").catch(() => {});
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
await reader.cancel(error).catch(() => {});
|
||||
throw error;
|
||||
} finally {
|
||||
rawBody += decoder.decode();
|
||||
reader.releaseLock();
|
||||
}
|
||||
|
||||
return rawBody;
|
||||
}
|
||||
|
||||
function isSemaphoreCapacityError(error: unknown): error is Error & { code: string } {
|
||||
return (
|
||||
!!error &&
|
||||
typeof error === "object" &&
|
||||
((error as { code?: unknown }).code === "SEMAPHORE_TIMEOUT" ||
|
||||
(error as { code?: unknown }).code === "SEMAPHORE_QUEUE_FULL")
|
||||
);
|
||||
}
|
||||
|
||||
function createStreamingErrorResult(
|
||||
statusCode: number,
|
||||
message: string,
|
||||
code?: string,
|
||||
type?: string
|
||||
) {
|
||||
const errorBody = buildErrorBody(statusCode, message);
|
||||
if (code) {
|
||||
errorBody.error.code = code;
|
||||
}
|
||||
if (type) {
|
||||
errorBody.error.type = type;
|
||||
}
|
||||
|
||||
const body = `data: ${JSON.stringify(errorBody)}\n\ndata: [DONE]\n\n`;
|
||||
|
||||
return {
|
||||
success: false as const,
|
||||
status: statusCode,
|
||||
error: message,
|
||||
response: new Response(body, {
|
||||
status: statusCode,
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache, no-transform",
|
||||
Connection: "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function getUpstreamErrorIdentifier(error: unknown): string | undefined {
|
||||
if (!error || typeof error !== "object") return undefined;
|
||||
const value = (error as { code?: unknown }).code;
|
||||
return typeof value === "string" && value.length > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function wrapReadableStreamWithFinalize<T>(
|
||||
readable: ReadableStream<T>,
|
||||
finalize: () => void
|
||||
): ReadableStream<T> {
|
||||
const reader = readable.getReader();
|
||||
let finalized = false;
|
||||
|
||||
const runFinalize = () => {
|
||||
if (finalized) return;
|
||||
finalized = true;
|
||||
finalize();
|
||||
};
|
||||
|
||||
return new ReadableStream<T>({
|
||||
async pull(controller) {
|
||||
try {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
runFinalize();
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
controller.enqueue(value);
|
||||
} catch (error) {
|
||||
runFinalize();
|
||||
controller.error(error);
|
||||
}
|
||||
},
|
||||
|
||||
async cancel(reason) {
|
||||
runFinalize();
|
||||
try {
|
||||
await reader.cancel(reason);
|
||||
} catch (error) {
|
||||
// Ignored
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function toPositiveNumber(value: unknown) {
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
||||
}
|
||||
|
||||
function buildCacheUsageLogMeta(usage: Record<string, unknown> | null | undefined) {
|
||||
if (!usage || typeof usage !== "object") return null;
|
||||
const promptTokenDetails =
|
||||
usage.prompt_tokens_details && typeof usage.prompt_tokens_details === "object"
|
||||
? (usage.prompt_tokens_details as Record<string, unknown>)
|
||||
: undefined;
|
||||
const hasCacheFields =
|
||||
"cache_read_input_tokens" in usage ||
|
||||
"cached_tokens" in usage ||
|
||||
"cache_creation_input_tokens" in usage ||
|
||||
(!!promptTokenDetails &&
|
||||
("cached_tokens" in promptTokenDetails || "cache_creation_tokens" in promptTokenDetails));
|
||||
const cacheReadTokens = toPositiveNumber(
|
||||
usage.cache_read_input_tokens ?? usage.cached_tokens ?? promptTokenDetails?.cached_tokens
|
||||
);
|
||||
const cacheCreationTokens = toPositiveNumber(
|
||||
usage.cache_creation_input_tokens ?? promptTokenDetails?.cache_creation_tokens
|
||||
);
|
||||
if (!hasCacheFields) return null;
|
||||
return {
|
||||
cacheReadTokens,
|
||||
cacheCreationTokens,
|
||||
};
|
||||
}
|
||||
|
||||
function attachLogMeta(
|
||||
payload: Record<string, unknown> | null | undefined,
|
||||
meta: Record<string, unknown> | null | undefined
|
||||
) {
|
||||
if (!meta || typeof meta !== "object") return payload;
|
||||
const compactMeta = Object.fromEntries(
|
||||
Object.entries(meta).filter(([, value]) => value !== null && value !== undefined)
|
||||
);
|
||||
if (Object.keys(compactMeta).length === 0) return payload;
|
||||
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
||||
return { _omniroute: compactMeta, _payload: payload ?? null };
|
||||
}
|
||||
const existing =
|
||||
payload._omniroute &&
|
||||
typeof payload._omniroute === "object" &&
|
||||
!Array.isArray(payload._omniroute)
|
||||
? payload._omniroute
|
||||
: {};
|
||||
return {
|
||||
...payload,
|
||||
_omniroute: {
|
||||
...existing,
|
||||
...compactMeta,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Core chat handler - shared between SSE and Worker
|
||||
* Returns { success, response, status, error } for caller to handle fallback
|
||||
@@ -514,33 +308,6 @@ function attachLogMeta(
|
||||
* @param {string} options.connectionId - Connection ID for settings lookup
|
||||
*/
|
||||
|
||||
function buildExecutorClientHeaders(
|
||||
headers: Headers | Record<string, unknown> | null | undefined,
|
||||
userAgent?: string | null
|
||||
) {
|
||||
const normalized: Record<string, string> = {};
|
||||
|
||||
if (headers instanceof Headers) {
|
||||
headers.forEach((value, key) => {
|
||||
normalized[key] = value;
|
||||
});
|
||||
} else if (headers && typeof headers === "object") {
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (typeof value === "string") {
|
||||
normalized[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedUserAgent = typeof userAgent === "string" ? userAgent.trim() : "";
|
||||
if (normalizedUserAgent && !normalized["user-agent"] && !normalized["User-Agent"]) {
|
||||
normalized["user-agent"] = normalizedUserAgent;
|
||||
normalized["User-Agent"] = normalizedUserAgent;
|
||||
}
|
||||
|
||||
return Object.keys(normalized).length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
// extractSystemRoleMessages extracted to chatCore/claudeSystemRole.ts (#3501); re-exported above so
|
||||
// existing importers (e.g. tests/unit/system-role-extraction.test.ts) keep resolving it from here.
|
||||
|
||||
|
||||
65
open-sse/handlers/chatCore/cacheUsageMeta.ts
Normal file
65
open-sse/handlers/chatCore/cacheUsageMeta.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* chatCore cache-usage log meta helpers (Quality Gate v2 / Fase 9 — chatCore god-file decomposition,
|
||||
* #3501).
|
||||
*
|
||||
* Pure helpers extracted from chatCore: coerce an unknown to a positive number, derive cache
|
||||
* read/creation token counts from a usage object (handling both top-level and prompt_tokens_details
|
||||
* shapes), and attach an `_omniroute` meta blob to a log payload. Side-effect-free; behaviour is
|
||||
* byte-identical to the previous module-level functions.
|
||||
*/
|
||||
|
||||
export function toPositiveNumber(value: unknown) {
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
||||
}
|
||||
|
||||
export function buildCacheUsageLogMeta(usage: Record<string, unknown> | null | undefined) {
|
||||
if (!usage || typeof usage !== "object") return null;
|
||||
const promptTokenDetails =
|
||||
usage.prompt_tokens_details && typeof usage.prompt_tokens_details === "object"
|
||||
? (usage.prompt_tokens_details as Record<string, unknown>)
|
||||
: undefined;
|
||||
const hasCacheFields =
|
||||
"cache_read_input_tokens" in usage ||
|
||||
"cached_tokens" in usage ||
|
||||
"cache_creation_input_tokens" in usage ||
|
||||
(!!promptTokenDetails &&
|
||||
("cached_tokens" in promptTokenDetails || "cache_creation_tokens" in promptTokenDetails));
|
||||
const cacheReadTokens = toPositiveNumber(
|
||||
usage.cache_read_input_tokens ?? usage.cached_tokens ?? promptTokenDetails?.cached_tokens
|
||||
);
|
||||
const cacheCreationTokens = toPositiveNumber(
|
||||
usage.cache_creation_input_tokens ?? promptTokenDetails?.cache_creation_tokens
|
||||
);
|
||||
if (!hasCacheFields) return null;
|
||||
return {
|
||||
cacheReadTokens,
|
||||
cacheCreationTokens,
|
||||
};
|
||||
}
|
||||
|
||||
export function attachLogMeta(
|
||||
payload: Record<string, unknown> | null | undefined,
|
||||
meta: Record<string, unknown> | null | undefined
|
||||
) {
|
||||
if (!meta || typeof meta !== "object") return payload;
|
||||
const compactMeta = Object.fromEntries(
|
||||
Object.entries(meta).filter(([, value]) => value !== null && value !== undefined)
|
||||
);
|
||||
if (Object.keys(compactMeta).length === 0) return payload;
|
||||
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
||||
return { _omniroute: compactMeta, _payload: payload ?? null };
|
||||
}
|
||||
const existing =
|
||||
payload._omniroute &&
|
||||
typeof payload._omniroute === "object" &&
|
||||
!Array.isArray(payload._omniroute)
|
||||
? payload._omniroute
|
||||
: {};
|
||||
return {
|
||||
...payload,
|
||||
_omniroute: {
|
||||
...existing,
|
||||
...compactMeta,
|
||||
},
|
||||
};
|
||||
}
|
||||
36
open-sse/handlers/chatCore/executorClientHeaders.ts
Normal file
36
open-sse/handlers/chatCore/executorClientHeaders.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* chatCore executor client-header normalizer (Quality Gate v2 / Fase 9 — chatCore god-file
|
||||
* decomposition, #3501).
|
||||
*
|
||||
* Pure helper extracted from chatCore: normalizes a Headers instance or a plain header object into a
|
||||
* lowercased-tolerant Record<string,string>, and backfills the client User-Agent (both casings) when
|
||||
* one is supplied and not already present. Returns null when nothing was collected. Side-effect-free;
|
||||
* behaviour is byte-identical to the previous module-level function.
|
||||
*/
|
||||
|
||||
export function buildExecutorClientHeaders(
|
||||
headers: Headers | Record<string, unknown> | null | undefined,
|
||||
userAgent?: string | null
|
||||
) {
|
||||
const normalized: Record<string, string> = {};
|
||||
|
||||
if (headers instanceof Headers) {
|
||||
headers.forEach((value, key) => {
|
||||
normalized[key] = value;
|
||||
});
|
||||
} else if (headers && typeof headers === "object") {
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (typeof value === "string") {
|
||||
normalized[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedUserAgent = typeof userAgent === "string" ? userAgent.trim() : "";
|
||||
if (normalizedUserAgent && !normalized["user-agent"] && !normalized["User-Agent"]) {
|
||||
normalized["user-agent"] = normalizedUserAgent;
|
||||
normalized["User-Agent"] = normalizedUserAgent;
|
||||
}
|
||||
|
||||
return Object.keys(normalized).length > 0 ? normalized : null;
|
||||
}
|
||||
68
open-sse/handlers/chatCore/nonStreamingResponseBody.ts
Normal file
68
open-sse/handlers/chatCore/nonStreamingResponseBody.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* chatCore non-streaming response-body reader (Quality Gate v2 / Fase 9 — chatCore god-file
|
||||
* decomposition, #3501).
|
||||
*
|
||||
* Extracted from chatCore: reads an upstream response body to a string. When the upstream is an SSE /
|
||||
* NDJSON stream consumed in non-streaming mode, it drains the reader chunk-by-chunk under the body
|
||||
* timeout and cancels early once a terminal SSE signal is observed; otherwise it falls back to a
|
||||
* timeout-bounded response.text(). Behaviour is byte-identical to the previous module-level function.
|
||||
*/
|
||||
|
||||
import { withBodyTimeout } from "../../utils/stream.ts";
|
||||
import { FETCH_BODY_TIMEOUT_MS } from "../../config/constants.ts";
|
||||
import { createBodyTimeoutError, readStreamChunkWithTimeout } from "./upstreamTimeouts.ts";
|
||||
import {
|
||||
appendNonStreamingSseTerminalSignal,
|
||||
type NonStreamingSseTerminalState,
|
||||
} from "./nonStreamingSse.ts";
|
||||
|
||||
export async function readNonStreamingResponseBody(
|
||||
response: Response,
|
||||
contentType: string,
|
||||
upstreamStream: boolean
|
||||
): Promise<string> {
|
||||
if (
|
||||
!upstreamStream ||
|
||||
!response.body ||
|
||||
(!contentType.includes("text/event-stream") && !contentType.includes("application/x-ndjson"))
|
||||
) {
|
||||
return withBodyTimeout<string>(response.text());
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
const terminalState: NonStreamingSseTerminalState = {
|
||||
currentEvent: "",
|
||||
pendingLine: "",
|
||||
};
|
||||
let rawBody = "";
|
||||
const deadline = FETCH_BODY_TIMEOUT_MS > 0 ? Date.now() + FETCH_BODY_TIMEOUT_MS : 0;
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const timeoutMs = deadline > 0 ? deadline - Date.now() : 0;
|
||||
if (deadline > 0 && timeoutMs <= 0) {
|
||||
throw createBodyTimeoutError(FETCH_BODY_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
const { done, value } = await readStreamChunkWithTimeout(reader, timeoutMs);
|
||||
if (done) break;
|
||||
if (!value) continue;
|
||||
|
||||
const decodedChunk = decoder.decode(value, { stream: true });
|
||||
rawBody += decodedChunk;
|
||||
if (appendNonStreamingSseTerminalSignal(terminalState, decodedChunk)) {
|
||||
await reader.cancel("non-streaming bridge consumed terminal SSE event").catch(() => {});
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
await reader.cancel(error).catch(() => {});
|
||||
throw error;
|
||||
} finally {
|
||||
rawBody += decoder.decode();
|
||||
reader.releaseLock();
|
||||
}
|
||||
|
||||
return rawBody;
|
||||
}
|
||||
33
open-sse/handlers/chatCore/skillsFormat.ts
Normal file
33
open-sse/handlers/chatCore/skillsFormat.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* chatCore skills-format mappers (Quality Gate v2 / Fase 9 — chatCore god-file decomposition, #3501).
|
||||
*
|
||||
* Pure mappers extracted from chatCore: translate the request wire format into the skills provider
|
||||
* bucket and the skills model id used when injecting skill context. Side-effect-free; behaviour is
|
||||
* byte-identical to the previous module-level functions.
|
||||
*/
|
||||
|
||||
import { FORMATS } from "../../translator/formats.ts";
|
||||
|
||||
export function getSkillsProviderForFormat(
|
||||
format: string
|
||||
): "openai" | "anthropic" | "google" | "other" {
|
||||
switch (format) {
|
||||
case FORMATS.CLAUDE:
|
||||
return "anthropic";
|
||||
case FORMATS.GEMINI:
|
||||
return "google";
|
||||
default:
|
||||
return "openai";
|
||||
}
|
||||
}
|
||||
|
||||
export function getSkillsModelIdForFormat(format: string): string {
|
||||
switch (format) {
|
||||
case FORMATS.CLAUDE:
|
||||
return "claude";
|
||||
case FORMATS.GEMINI:
|
||||
return "gemini";
|
||||
default:
|
||||
return "openai";
|
||||
}
|
||||
}
|
||||
58
open-sse/handlers/chatCore/streamErrorResult.ts
Normal file
58
open-sse/handlers/chatCore/streamErrorResult.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* chatCore streaming error-result helpers (Quality Gate v2 / Fase 9 — chatCore god-file
|
||||
* decomposition, #3501).
|
||||
*
|
||||
* Extracted from chatCore: identify semaphore capacity errors, build a sanitized SSE error result
|
||||
* (an `data: {...}\n\ndata: [DONE]\n\n` body wrapped in an event-stream Response), and pull a string
|
||||
* error code off an unknown error. Side-effect-free; behaviour is byte-identical to the previous
|
||||
* module-level functions.
|
||||
*/
|
||||
|
||||
import { buildErrorBody } from "../../utils/error.ts";
|
||||
|
||||
export function isSemaphoreCapacityError(error: unknown): error is Error & { code: string } {
|
||||
return (
|
||||
!!error &&
|
||||
typeof error === "object" &&
|
||||
((error as { code?: unknown }).code === "SEMAPHORE_TIMEOUT" ||
|
||||
(error as { code?: unknown }).code === "SEMAPHORE_QUEUE_FULL")
|
||||
);
|
||||
}
|
||||
|
||||
export function createStreamingErrorResult(
|
||||
statusCode: number,
|
||||
message: string,
|
||||
code?: string,
|
||||
type?: string
|
||||
) {
|
||||
const errorBody = buildErrorBody(statusCode, message);
|
||||
if (code) {
|
||||
errorBody.error.code = code;
|
||||
}
|
||||
if (type) {
|
||||
errorBody.error.type = type;
|
||||
}
|
||||
|
||||
const body = `data: ${JSON.stringify(errorBody)}\n\ndata: [DONE]\n\n`;
|
||||
|
||||
return {
|
||||
success: false as const,
|
||||
status: statusCode,
|
||||
error: message,
|
||||
response: new Response(body, {
|
||||
status: statusCode,
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache, no-transform",
|
||||
Connection: "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function getUpstreamErrorIdentifier(error: unknown): string | undefined {
|
||||
if (!error || typeof error !== "object") return undefined;
|
||||
const value = (error as { code?: unknown }).code;
|
||||
return typeof value === "string" && value.length > 0 ? value : undefined;
|
||||
}
|
||||
48
open-sse/handlers/chatCore/streamFinalize.ts
Normal file
48
open-sse/handlers/chatCore/streamFinalize.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* chatCore stream finalize wrapper (Quality Gate v2 / Fase 9 — chatCore god-file decomposition,
|
||||
* #3501).
|
||||
*
|
||||
* Extracted from chatCore: wraps a ReadableStream so a `finalize` callback runs exactly once when the
|
||||
* stream is fully drained, errors, or is cancelled. Side-effect-free other than the wrapped stream's
|
||||
* own lifecycle; behaviour is byte-identical to the previous module-level function.
|
||||
*/
|
||||
|
||||
export function wrapReadableStreamWithFinalize<T>(
|
||||
readable: ReadableStream<T>,
|
||||
finalize: () => void
|
||||
): ReadableStream<T> {
|
||||
const reader = readable.getReader();
|
||||
let finalized = false;
|
||||
|
||||
const runFinalize = () => {
|
||||
if (finalized) return;
|
||||
finalized = true;
|
||||
finalize();
|
||||
};
|
||||
|
||||
return new ReadableStream<T>({
|
||||
async pull(controller) {
|
||||
try {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
runFinalize();
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
controller.enqueue(value);
|
||||
} catch (error) {
|
||||
runFinalize();
|
||||
controller.error(error);
|
||||
}
|
||||
},
|
||||
|
||||
async cancel(reason) {
|
||||
runFinalize();
|
||||
try {
|
||||
await reader.cancel(reason);
|
||||
} catch (error) {
|
||||
// Ignored
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
58
tests/unit/chatcore-cache-usage-meta.test.ts
Normal file
58
tests/unit/chatcore-cache-usage-meta.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
// tests/unit/chatcore-cache-usage-meta.test.ts
|
||||
// Characterization of toPositiveNumber / buildCacheUsageLogMeta / attachLogMeta — cache-usage log
|
||||
// meta helpers extracted from handleChatCore (chatCore god-file decomposition, #3501). Locks: the
|
||||
// positive-number coercion, the cache-token derivation across top-level and prompt_tokens_details
|
||||
// shapes (null when no cache fields), and the _omniroute meta attachment/merge.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
toPositiveNumber,
|
||||
buildCacheUsageLogMeta,
|
||||
attachLogMeta,
|
||||
} from "../../open-sse/handlers/chatCore/cacheUsageMeta.ts";
|
||||
|
||||
test("toPositiveNumber keeps finite positives, zeros everything else", () => {
|
||||
assert.equal(toPositiveNumber(5), 5);
|
||||
assert.equal(toPositiveNumber(0), 0);
|
||||
assert.equal(toPositiveNumber(-3), 0);
|
||||
assert.equal(toPositiveNumber(Infinity), 0);
|
||||
assert.equal(toPositiveNumber("5"), 0);
|
||||
assert.equal(toPositiveNumber(null), 0);
|
||||
});
|
||||
|
||||
test("buildCacheUsageLogMeta returns null when there are no cache fields", () => {
|
||||
assert.equal(buildCacheUsageLogMeta(null), null);
|
||||
assert.equal(buildCacheUsageLogMeta({ prompt_tokens: 10 }), null);
|
||||
});
|
||||
|
||||
test("buildCacheUsageLogMeta reads top-level cache fields", () => {
|
||||
const meta = buildCacheUsageLogMeta({
|
||||
cache_read_input_tokens: 12,
|
||||
cache_creation_input_tokens: 4,
|
||||
});
|
||||
assert.deepEqual(meta, { cacheReadTokens: 12, cacheCreationTokens: 4 });
|
||||
});
|
||||
|
||||
test("buildCacheUsageLogMeta reads prompt_tokens_details shapes", () => {
|
||||
const meta = buildCacheUsageLogMeta({
|
||||
prompt_tokens_details: { cached_tokens: 7, cache_creation_tokens: 2 },
|
||||
});
|
||||
assert.deepEqual(meta, { cacheReadTokens: 7, cacheCreationTokens: 2 });
|
||||
});
|
||||
|
||||
test("attachLogMeta returns the payload untouched when meta is empty", () => {
|
||||
const payload = { a: 1 };
|
||||
assert.equal(attachLogMeta(payload, null), payload);
|
||||
assert.equal(attachLogMeta(payload, {}), payload);
|
||||
assert.equal(attachLogMeta(payload, { x: null, y: undefined }), payload);
|
||||
});
|
||||
|
||||
test("attachLogMeta merges compact meta into _omniroute", () => {
|
||||
const out = attachLogMeta({ a: 1, _omniroute: { keep: true } }, { added: 2, drop: null });
|
||||
assert.deepEqual(out, { a: 1, _omniroute: { keep: true, added: 2 } });
|
||||
});
|
||||
|
||||
test("attachLogMeta wraps non-object payloads", () => {
|
||||
const out = attachLogMeta(null, { added: 2 });
|
||||
assert.deepEqual(out, { _omniroute: { added: 2 }, _payload: null });
|
||||
});
|
||||
42
tests/unit/chatcore-executor-client-headers.test.ts
Normal file
42
tests/unit/chatcore-executor-client-headers.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// tests/unit/chatcore-executor-client-headers.test.ts
|
||||
// Characterization of buildExecutorClientHeaders — the executor client-header normalizer extracted
|
||||
// from handleChatCore (chatCore god-file decomposition, #3501). Locks: Headers and plain-object
|
||||
// normalization, non-string value skipping, User-Agent backfill (both casings, only when absent),
|
||||
// and the null-when-empty return.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { buildExecutorClientHeaders } from "../../open-sse/handlers/chatCore/executorClientHeaders.ts";
|
||||
|
||||
test("returns null for empty / nullish inputs", () => {
|
||||
assert.equal(buildExecutorClientHeaders(null), null);
|
||||
assert.equal(buildExecutorClientHeaders(undefined), null);
|
||||
assert.equal(buildExecutorClientHeaders({}), null);
|
||||
});
|
||||
|
||||
test("normalizes a Headers instance", () => {
|
||||
const h = new Headers({ "X-Test": "1", "content-type": "application/json" });
|
||||
const out = buildExecutorClientHeaders(h);
|
||||
assert.equal(out?.["content-type"], "application/json");
|
||||
assert.equal(out?.["x-test"], "1");
|
||||
});
|
||||
|
||||
test("normalizes a plain object and skips non-string values", () => {
|
||||
const out = buildExecutorClientHeaders({ a: "1", b: 2, c: null } as Record<string, unknown>);
|
||||
assert.deepEqual(out, { a: "1" });
|
||||
});
|
||||
|
||||
test("backfills the User-Agent in both casings when absent", () => {
|
||||
const out = buildExecutorClientHeaders({ a: "1" }, " MyAgent/1.0 ");
|
||||
assert.equal(out?.["user-agent"], "MyAgent/1.0");
|
||||
assert.equal(out?.["User-Agent"], "MyAgent/1.0");
|
||||
});
|
||||
|
||||
test("does not overwrite an existing user-agent header", () => {
|
||||
const out = buildExecutorClientHeaders({ "user-agent": "Existing/9" }, "MyAgent/1.0");
|
||||
assert.equal(out?.["user-agent"], "Existing/9");
|
||||
assert.equal(out?.["User-Agent"], undefined);
|
||||
});
|
||||
|
||||
test("a trimmed-empty user agent does not create headers on its own", () => {
|
||||
assert.equal(buildExecutorClientHeaders({}, " "), null);
|
||||
});
|
||||
33
tests/unit/chatcore-non-streaming-response-body.test.ts
Normal file
33
tests/unit/chatcore-non-streaming-response-body.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
// tests/unit/chatcore-non-streaming-response-body.test.ts
|
||||
// Characterization of readNonStreamingResponseBody — the non-streaming body reader extracted from
|
||||
// handleChatCore (chatCore god-file decomposition, #3501). Locks: the response.text() fallback path
|
||||
// (non-stream, or non-SSE content type) and the SSE-drain path that concatenates chunks until the
|
||||
// stream closes.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readNonStreamingResponseBody } from "../../open-sse/handlers/chatCore/nonStreamingResponseBody.ts";
|
||||
|
||||
test("falls back to response.text() when upstream is not streaming", async () => {
|
||||
const out = await readNonStreamingResponseBody(new Response("hello"), "application/json", false);
|
||||
assert.equal(out, "hello");
|
||||
});
|
||||
|
||||
test("falls back to response.text() for a non-SSE content type even when streaming", async () => {
|
||||
const out = await readNonStreamingResponseBody(new Response("plain"), "application/json", true);
|
||||
assert.equal(out, "plain");
|
||||
});
|
||||
|
||||
test("drains an SSE stream chunk-by-chunk and concatenates until close", async () => {
|
||||
const enc = new TextEncoder();
|
||||
const body = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(enc.encode("data: {\"a\":1}\n\n"));
|
||||
controller.enqueue(enc.encode("data: {\"b\":2}\n\n"));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
const response = new Response(body, { headers: { "Content-Type": "text/event-stream" } });
|
||||
const out = await readNonStreamingResponseBody(response, "text/event-stream", true);
|
||||
assert.ok(out.includes('"a":1'));
|
||||
assert.ok(out.includes('"b":2'));
|
||||
});
|
||||
32
tests/unit/chatcore-skills-format.test.ts
Normal file
32
tests/unit/chatcore-skills-format.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// tests/unit/chatcore-skills-format.test.ts
|
||||
// Characterization of getSkillsProviderForFormat / getSkillsModelIdForFormat — the skills-format
|
||||
// mappers extracted from handleChatCore (chatCore god-file decomposition, #3501). Locks the
|
||||
// claude→anthropic/claude, gemini→google/gemini and default→openai/openai mappings.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
getSkillsProviderForFormat,
|
||||
getSkillsModelIdForFormat,
|
||||
} from "../../open-sse/handlers/chatCore/skillsFormat.ts";
|
||||
|
||||
test("claude format maps to anthropic provider and claude model id", () => {
|
||||
assert.equal(getSkillsProviderForFormat("claude"), "anthropic");
|
||||
assert.equal(getSkillsModelIdForFormat("claude"), "claude");
|
||||
});
|
||||
|
||||
test("gemini format maps to google provider and gemini model id", () => {
|
||||
assert.equal(getSkillsProviderForFormat("gemini"), "google");
|
||||
assert.equal(getSkillsModelIdForFormat("gemini"), "gemini");
|
||||
});
|
||||
|
||||
test("openai format maps to openai provider and openai model id", () => {
|
||||
assert.equal(getSkillsProviderForFormat("openai"), "openai");
|
||||
assert.equal(getSkillsModelIdForFormat("openai"), "openai");
|
||||
});
|
||||
|
||||
test("unknown / responses formats fall back to openai for both mappers", () => {
|
||||
assert.equal(getSkillsProviderForFormat("openai-responses"), "openai");
|
||||
assert.equal(getSkillsModelIdForFormat("openai-responses"), "openai");
|
||||
assert.equal(getSkillsProviderForFormat("gemini-cli"), "openai");
|
||||
assert.equal(getSkillsModelIdForFormat("totally-unknown"), "openai");
|
||||
});
|
||||
52
tests/unit/chatcore-stream-error-result.test.ts
Normal file
52
tests/unit/chatcore-stream-error-result.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
// tests/unit/chatcore-stream-error-result.test.ts
|
||||
// Characterization of isSemaphoreCapacityError / createStreamingErrorResult /
|
||||
// getUpstreamErrorIdentifier — streaming error-result helpers extracted from handleChatCore
|
||||
// (chatCore god-file decomposition, #3501). Locks the semaphore code matching, the SSE error
|
||||
// envelope shape (status, headers, `data: {...}\n\ndata: [DONE]\n\n` body, optional code/type), and
|
||||
// the string-code extraction.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
isSemaphoreCapacityError,
|
||||
createStreamingErrorResult,
|
||||
getUpstreamErrorIdentifier,
|
||||
} from "../../open-sse/handlers/chatCore/streamErrorResult.ts";
|
||||
|
||||
test("isSemaphoreCapacityError matches the two semaphore codes only", () => {
|
||||
assert.equal(isSemaphoreCapacityError({ code: "SEMAPHORE_TIMEOUT" }), true);
|
||||
assert.equal(isSemaphoreCapacityError({ code: "SEMAPHORE_QUEUE_FULL" }), true);
|
||||
assert.equal(isSemaphoreCapacityError({ code: "OTHER" }), false);
|
||||
assert.equal(isSemaphoreCapacityError(null), false);
|
||||
assert.equal(isSemaphoreCapacityError("SEMAPHORE_TIMEOUT"), false);
|
||||
});
|
||||
|
||||
test("createStreamingErrorResult builds an SSE error envelope with [DONE] terminator", async () => {
|
||||
const result = createStreamingErrorResult(503, "boom");
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 503);
|
||||
assert.equal(result.error, "boom");
|
||||
assert.equal(result.response.status, 503);
|
||||
assert.equal(result.response.headers.get("Content-Type"), "text/event-stream");
|
||||
assert.equal(result.response.headers.get("X-Accel-Buffering"), "no");
|
||||
const body = await result.response.text();
|
||||
assert.ok(body.startsWith("data: "));
|
||||
assert.ok(body.endsWith("data: [DONE]\n\n"));
|
||||
const json = JSON.parse(body.slice("data: ".length, body.indexOf("\n\n")));
|
||||
assert.equal(json.error.message, "boom");
|
||||
});
|
||||
|
||||
test("createStreamingErrorResult attaches optional code and type", async () => {
|
||||
const result = createStreamingErrorResult(429, "slow down", "rate_limited", "rate_limit_error");
|
||||
const body = await result.response.text();
|
||||
const json = JSON.parse(body.slice("data: ".length, body.indexOf("\n\n")));
|
||||
assert.equal(json.error.code, "rate_limited");
|
||||
assert.equal(json.error.type, "rate_limit_error");
|
||||
});
|
||||
|
||||
test("getUpstreamErrorIdentifier returns a non-empty string code or undefined", () => {
|
||||
assert.equal(getUpstreamErrorIdentifier({ code: "ECONNRESET" }), "ECONNRESET");
|
||||
assert.equal(getUpstreamErrorIdentifier({ code: "" }), undefined);
|
||||
assert.equal(getUpstreamErrorIdentifier({ code: 123 }), undefined);
|
||||
assert.equal(getUpstreamErrorIdentifier(null), undefined);
|
||||
assert.equal(getUpstreamErrorIdentifier("ECONNRESET"), undefined);
|
||||
});
|
||||
47
tests/unit/chatcore-stream-finalize.test.ts
Normal file
47
tests/unit/chatcore-stream-finalize.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// tests/unit/chatcore-stream-finalize.test.ts
|
||||
// Characterization of wrapReadableStreamWithFinalize — the stream finalize wrapper extracted from
|
||||
// handleChatCore (chatCore god-file decomposition, #3501). Locks: finalize runs exactly once on
|
||||
// full drain, on cancel, and is not double-invoked.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { wrapReadableStreamWithFinalize } from "../../open-sse/handlers/chatCore/streamFinalize.ts";
|
||||
|
||||
function streamOf(chunks: unknown[]): ReadableStream {
|
||||
let i = 0;
|
||||
return new ReadableStream({
|
||||
pull(controller) {
|
||||
if (i < chunks.length) {
|
||||
controller.enqueue(chunks[i++]);
|
||||
} else {
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
test("finalize runs exactly once after the stream is fully drained", async () => {
|
||||
let calls = 0;
|
||||
const wrapped = wrapReadableStreamWithFinalize(streamOf(["a", "b"]), () => {
|
||||
calls++;
|
||||
});
|
||||
const reader = wrapped.getReader();
|
||||
const seen: unknown[] = [];
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
seen.push(value);
|
||||
}
|
||||
assert.deepEqual(seen, ["a", "b"]);
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
|
||||
test("finalize runs exactly once on cancel", async () => {
|
||||
let calls = 0;
|
||||
const wrapped = wrapReadableStreamWithFinalize(streamOf(["a", "b", "c"]), () => {
|
||||
calls++;
|
||||
});
|
||||
const reader = wrapped.getReader();
|
||||
await reader.read();
|
||||
await reader.cancel("done early");
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
Reference in New Issue
Block a user