import { Readable } from "node:stream";
import { isJsonObject, jsonClone } from "../types/json.js";
import { convertMessagesToBridgeInput } from "../../bridge-message-utils.js";
import { VirtualRouterEngine } from "../../../router/virtual-router/engine.js";
import { providerErrorCenter } from "../../../router/virtual-router/error-center.js";
import { providerSuccessCenter } from "../../../router/virtual-router/success-center.js";
import { defaultSseCodecRegistry, } from "../../../sse/index.js";
import { ResponsesFormatAdapter } from "../format-adapters/responses-format-adapter.js";
import { ResponsesSemanticMapper } from "../semantic-mappers/responses-mapper.js";
import { AnthropicFormatAdapter } from "../format-adapters/anthropic-format-adapter.js";
import { AnthropicSemanticMapper } from "../semantic-mappers/anthropic-mapper.js";
import { GeminiFormatAdapter } from "../format-adapters/gemini-format-adapter.js";
import { GeminiSemanticMapper } from "../semantic-mappers/gemini-mapper.js";
import { ChatFormatAdapter } from "../format-adapters/chat-format-adapter.js";
import { ChatSemanticMapper } from "../semantic-mappers/chat-mapper.js";
import { createSnapshotRecorder } from "../snapshot-recorder.js";
import { shouldRecordSnapshots } from "../../snapshot-utils.js";
import { runReqInboundStage1FormatParse } from "./stages/req_inbound/req_inbound_stage1_format_parse/index.js";
import { runReqInboundStage2SemanticMap } from "./stages/req_inbound/req_inbound_stage2_semantic_map/index.js";
import { runChatContextCapture, captureResponsesContextSnapshot, } from "./stages/req_inbound/req_inbound_stage3_context_capture/index.js";
import { normalizeReqInboundToolCallIdStyleWithNative } from "../../../router/virtual-router/engine-selection/native-hub-pipeline-req-inbound-semantics.js";
import { createResponsesContextCapture, createNoopContextCapture, } from "./stages/req_inbound/req_inbound_stage3_context_capture/context-factories.js";
import { runReqProcessStage1ToolGovernance } from "./stages/req_process/req_process_stage1_tool_governance/index.js";
import { runReqProcessStage2RouteSelect } from "./stages/req_process/req_process_stage2_route_select/index.js";
import { runReqOutboundStage1SemanticMap } from "./stages/req_outbound/req_outbound_stage1_semantic_map/index.js";
import { runReqOutboundStage2FormatBuild } from "./stages/req_outbound/req_outbound_stage2_format_build/index.js";
import { runReqOutboundStage3Compat } from "./stages/req_outbound/req_outbound_stage3_compat/index.js";
import { extractSessionIdentifiersFromMetadata } from "./session-identifiers.js";
import { computeRequestTokens } from "../../../router/virtual-router/token-estimator.js";
import { estimateSessionBoundTokens } from "../process/chat-process-session-usage.js";
import { annotatePassthroughGovernanceSkipWithNative, attachPassthroughProviderInputAuditWithNative, buildPassthroughAuditWithNative, applyOutboundStreamPreferenceWithNative, normalizeHubEndpointWithNative, extractAdapterContextMetadataFieldsWithNative, resolveApplyPatchToolModeFromToolsWithNative, resolveHubClientProtocolWithNative, resolveHubPolicyOverrideFromMetadataWithNative, resolveHubProviderProtocolWithNative, resolveOutboundStreamIntentWithNative, resolveHubShadowCompareConfigWithNative, resolveActiveProcessModeWithNative, findMappableSemanticsKeysWithNative, resolveHubSseProtocolFromMetadataWithNative, resolveStopMessageRouterMetadataWithNative, runHubPipelineOrchestrationWithNative, } from "../../../router/virtual-router/engine-selection/native-hub-pipeline-orchestration-semantics.js";
import { normalizeAliasMapWithNative, resolveAliasMapFromRespSemanticsWithNative, } from "../../../router/virtual-router/engine-selection/native-hub-pipeline-resp-semantics.js";
import { isCompactionRequest } from "../../compaction-detect.js";
import { applyHubProviderOutboundPolicy, recordHubPolicyObservation, setHubPolicyRuntimePolicy, } from "../policy/policy-engine.js";
import { applyProviderOutboundToolSurface, } from "../tool-surface/tool-surface-engine.js";
import { cloneRuntimeMetadata, ensureRuntimeMetadata, readRuntimeMetadata, } from "../../runtime-metadata.js";
import { containsImageAttachment, stripHistoricalImageAttachments, stripHistoricalVisualToolOutputs, repairIncompleteToolCalls, } from "../process/chat-process-media.js";
import { measureHubStage, logHubStageTiming } from "./hub-stage-timing.js";
function isTruthyEnv(value) {
    const v = typeof value === "string" ? value.trim().toLowerCase() : "";
    return v === "1" || v === "true" || v === "yes" || v === "on";
}
function resolveApplyPatchToolModeFromEnv() {
    const rawMode = String(process.env.RCC_APPLY_PATCH_TOOL_MODE ||
        process.env.ROUTECODEX_APPLY_PATCH_TOOL_MODE ||
        "")
        .trim()
        .toLowerCase();
    if (rawMode === "freeform")
        return "freeform";
    if (rawMode === "schema" || rawMode === "json_schema")
        return "schema";
    const freeformFlag = process.env.RCC_APPLY_PATCH_FREEFORM ||
        process.env.ROUTECODEX_APPLY_PATCH_FREEFORM;
    if (isTruthyEnv(freeformFlag))
        return "freeform";
    return undefined;
}
function applyChatProcessEntryMediaCleanup(request) {
    return {
        ...request,
        messages: repairIncompleteToolCalls(stripHistoricalVisualToolOutputs(stripHistoricalImageAttachments(request.messages))),
    };
}
function readResponsesResumeFromMetadata(metadata) {
    if (!metadata || typeof metadata !== "object") {
        return undefined;
    }
    const resume = metadata.responsesResume;
    return resume && isJsonObject(resume)
        ? resume
        : undefined;
}
function readResponsesResumeFromRequestSemantics(request) {
    try {
        const semantics = request?.semantics;
        const responses = semantics &&
            typeof semantics === "object" &&
            !Array.isArray(semantics) &&
            semantics.responses &&
            typeof semantics.responses === "object" &&
            !Array.isArray(semantics.responses)
            ? semantics.responses
            : undefined;
        const resume = responses &&
            responses.resume &&
            typeof responses.resume === "object" &&
            !Array.isArray(responses.resume)
            ? responses.resume
            : undefined;
        return resume;
    }
    catch {
        return undefined;
    }
}
function liftResponsesResumeIntoSemantics(request, metadata) {
    const resumeMeta = readResponsesResumeFromMetadata(metadata);
    if (!resumeMeta) {
        return request;
    }
    const next = {
        ...request,
        semantics: {
            ...(request.semantics ??
                {}),
        },
    };
    const semantics = next.semantics;
    if (!semantics.responses ||
        typeof semantics.responses !== "object" ||
        Array.isArray(semantics.responses)) {
        semantics.responses = {};
    }
    const responsesNode = semantics.responses;
    if (responsesNode.resume === undefined) {
        responsesNode.resume = jsonClone(resumeMeta);
    }
    delete metadata.responsesResume;
    return next;
}
function syncResponsesContextFromCanonicalMessages(request) {
    const semantics = request?.semantics;
    const responsesNode = semantics &&
        typeof semantics === "object" &&
        !Array.isArray(semantics) &&
        semantics.responses &&
        typeof semantics.responses === "object" &&
        !Array.isArray(semantics.responses)
        ? semantics.responses
        : undefined;
    const contextNode = responsesNode &&
        responsesNode.context &&
        typeof responsesNode.context === "object" &&
        !Array.isArray(responsesNode.context)
        ? responsesNode.context
        : undefined;
    if (!contextNode) {
        return request;
    }
    const bridge = convertMessagesToBridgeInput({
        messages: request.messages ?? [],
        tools: Array.isArray(request.tools)
            ? request.tools
            : undefined,
    });
    return {
        ...request,
        semantics: {
            ...semantics,
            responses: {
                ...responsesNode,
                context: {
                    ...contextNode,
                    input: jsonClone(bridge.input),
                    originalSystemMessages: jsonClone(bridge.originalSystemMessages),
                },
            },
        },
    };
}
function resolveApplyPatchToolModeFromTools(toolsRaw) {
    return resolveApplyPatchToolModeFromToolsWithNative(toolsRaw);
}
function extractHubPolicyOverride(metadata) {
    const parsed = resolveHubPolicyOverrideFromMetadataWithNative(metadata);
    if (!parsed) {
        return undefined;
    }
    return {
        mode: parsed.mode,
        ...(parsed.sampleRate !== undefined
            ? { sampleRate: parsed.sampleRate }
            : {}),
    };
}
function propagateAdapterContextMetadataFields(adapterContext, metadata, keys) {
    const picked = extractAdapterContextMetadataFieldsWithNative(metadata, keys);
    Object.assign(adapterContext, picked);
}
function resolveStopMessageRouterMetadata(metadata) {
    return resolveStopMessageRouterMetadataWithNative(metadata);
}
function isSearchRouteId(routeId) {
    const normalized = typeof routeId === "string" ? routeId.trim().toLowerCase() : "";
    return normalized.startsWith("web_search") || normalized.startsWith("search");
}
function isCanonicalWebSearchToolDefinition(tool) {
    if (!tool || typeof tool !== "object" || Array.isArray(tool)) {
        return false;
    }
    const row = tool;
    const rawType = typeof row.type === "string" ? row.type.trim().toLowerCase() : "";
    if (rawType === "web_search_20250305" || rawType === "web_search") {
        return true;
    }
    const fnNode = row.function &&
        typeof row.function === "object" &&
        !Array.isArray(row.function)
        ? row.function
        : undefined;
    const name = typeof fnNode?.name === "string"
        ? fnNode.name.trim().toLowerCase()
        : typeof row.name === "string"
            ? row.name.trim().toLowerCase()
            : "";
    return name === "web_search" || name === "websearch" || name === "web-search";
}
function maybeApplyDirectBuiltinWebSearchTool(providerPayload, adapterContext, providerProtocol) {
    if (providerProtocol !== "anthropic-messages") {
        return providerPayload;
    }
    if (!isSearchRouteId(adapterContext.routeId)) {
        return providerPayload;
    }
    const modelId = typeof providerPayload.model === "string"
        ? providerPayload.model.trim()
        : "";
    if (!modelId) {
        return providerPayload;
    }
    const rt = readRuntimeMetadata(adapterContext);
    const webSearch = rt &&
        typeof rt.webSearch === "object" &&
        rt.webSearch &&
        !Array.isArray(rt.webSearch)
        ? rt.webSearch
        : undefined;
    const enginesRaw = Array.isArray(webSearch?.engines)
        ? webSearch?.engines
        : [];
    const matchedEngine = enginesRaw.find((entry) => {
        if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
            return false;
        }
        const row = entry;
        const executionMode = typeof row.executionMode === "string"
            ? row.executionMode.trim().toLowerCase()
            : "";
        if (executionMode !== "direct") {
            return false;
        }
        const directActivation = typeof row.directActivation === "string"
            ? row.directActivation.trim().toLowerCase()
            : "route";
        if (directActivation !== "builtin") {
            return false;
        }
        const configuredModelId = typeof row.modelId === "string" ? row.modelId.trim() : "";
        if (configuredModelId && configuredModelId === modelId) {
            return true;
        }
        const providerKey = typeof row.providerKey === "string" ? row.providerKey.trim() : "";
        return providerKey.endsWith(`.${modelId}`);
    });
    if (!matchedEngine) {
        return providerPayload;
    }
    const rawMaxUses = typeof matchedEngine.maxUses === "number"
        ? matchedEngine.maxUses
        : Number(matchedEngine.maxUses);
    const maxUses = Number.isFinite(rawMaxUses) && rawMaxUses > 0 ? Math.floor(rawMaxUses) : 2;
    const builtinTool = {
        type: "web_search_20250305",
        name: "web_search",
        max_uses: maxUses,
    };
    const tools = Array.isArray(providerPayload.tools)
        ? providerPayload.tools
        : [];
    let replaced = false;
    const nextTools = [];
    for (const tool of tools) {
        if (!replaced && isCanonicalWebSearchToolDefinition(tool)) {
            nextTools.push(builtinTool);
            replaced = true;
            continue;
        }
        if (isCanonicalWebSearchToolDefinition(tool)) {
            continue;
        }
        nextTools.push(tool);
    }
    if (!replaced) {
        nextTools.unshift(builtinTool);
    }
    providerPayload.tools = nextTools;
    return providerPayload;
}
function extractHubShadowCompareConfig(metadata) {
    const parsed = resolveHubShadowCompareConfigWithNative(metadata);
    if (!parsed) {
        return undefined;
    }
    return { baselineMode: parsed.baselineMode };
}
function buildPassthroughAudit(rawInbound, providerProtocol) {
    return buildPassthroughAuditWithNative(rawInbound, providerProtocol);
}
function annotatePassthroughGovernanceSkip(audit) {
    const next = annotatePassthroughGovernanceSkipWithNative(audit);
    for (const key of Object.keys(audit)) {
        delete audit[key];
    }
    Object.assign(audit, next);
}
function attachPassthroughProviderInputAudit(audit, providerPayload, providerProtocol) {
    const next = attachPassthroughProviderInputAuditWithNative(audit, providerPayload, providerProtocol);
    for (const key of Object.keys(audit)) {
        delete audit[key];
    }
    Object.assign(audit, next);
}
function resolveActiveProcessMode(baseMode, messages) {
    return resolveActiveProcessModeWithNative(baseMode, messages);
}
export class HubPipeline {
    routerEngine;
    config;
    unsubscribeProviderErrors;
    unsubscribeProviderSuccess;
    constructor(config) {
        this.config = config;
        this.routerEngine = new VirtualRouterEngine({
            healthStore: config.healthStore,
            routingStateStore: config.routingStateStore,
            quotaView: config.quotaView,
        });
        this.routerEngine.initialize(config.virtualRouter);
        setHubPolicyRuntimePolicy(config.policy);
        try {
            this.unsubscribeProviderErrors = providerErrorCenter.subscribe((event) => {
                try {
                    this.routerEngine.handleProviderError(event);
                }
                catch {
                    // ignore subscriber errors
                }
            });
        }
        catch {
            this.unsubscribeProviderErrors = undefined;
        }
        try {
            this.unsubscribeProviderSuccess = providerSuccessCenter.subscribe((event) => {
                try {
                    this.routerEngine.handleProviderSuccess(event);
                }
                catch {
                    // ignore subscriber errors
                }
            });
        }
        catch {
            this.unsubscribeProviderSuccess = undefined;
        }
    }
    updateRuntimeDeps(deps) {
        if (!deps || typeof deps !== "object") {
            return;
        }
        if ("healthStore" in deps) {
            this.config.healthStore = deps.healthStore ?? undefined;
        }
        if ("routingStateStore" in deps) {
            this.config.routingStateStore = (deps.routingStateStore ??
                undefined);
        }
        if ("quotaView" in deps) {
            this.config.quotaView = deps.quotaView ?? undefined;
        }
        try {
            this.routerEngine.updateDeps({
                healthStore: this.config.healthStore ?? null,
                routingStateStore: (this.config.routingStateStore ?? null),
                quotaView: this.config.quotaView ?? null,
            });
        }
        catch {
            // best-effort: runtime deps updates must never break routing
        }
    }
    updateVirtualRouterConfig(nextConfig) {
        if (!nextConfig || typeof nextConfig !== "object") {
            throw new Error("HubPipeline updateVirtualRouterConfig requires VirtualRouterConfig payload");
        }
        this.config.virtualRouter = nextConfig;
        this.routerEngine.initialize(nextConfig);
    }
    dispose() {
        if (this.unsubscribeProviderErrors) {
            try {
                this.unsubscribeProviderErrors();
            }
            catch {
                // ignore dispose failures
            }
            this.unsubscribeProviderErrors = undefined;
        }
        if (this.unsubscribeProviderSuccess) {
            try {
                this.unsubscribeProviderSuccess();
            }
            catch {
                // ignore dispose failures
            }
            this.unsubscribeProviderSuccess = undefined;
        }
    }
    async executeRequestStagePipeline(normalized, hooks) {
        const semanticMapper = hooks.createSemanticMapper();
        const rawRequest = this.asJsonObject(normalized.payload);
        // Detect applyPatchToolMode (runtime/tooling hint). Client tool schemas are captured as chat semantics
        // in req_inbound_stage2_semantic_map; they must not be stored in metadata.
        try {
            const toolsRaw = Array.isArray(rawRequest?.tools)
                ? rawRequest.tools
                : null;
            const applyPatchToolMode = resolveApplyPatchToolModeFromEnv() ??
                resolveApplyPatchToolModeFromTools(toolsRaw);
            if (applyPatchToolMode) {
                normalized.metadata = normalized.metadata || {};
                const rt = ensureRuntimeMetadata(normalized.metadata);
                rt.applyPatchToolMode = applyPatchToolMode;
            }
        }
        catch {
            // best-effort: do not block request handling due to tool scan failures
        }
        if (isCompactionRequest(rawRequest)) {
            normalized.metadata = normalized.metadata || {};
            const rt = ensureRuntimeMetadata(normalized.metadata);
            rt.compactionRequest = true;
        }
        const effectivePolicy = normalized.policyOverride ?? this.config.policy;
        const shadowCompareBaselineMode = normalized.shadowCompare?.baselineMode;
        const inboundAdapterContext = this.buildAdapterContext(normalized);
        const inboundRecorder = this.maybeCreateStageRecorder(inboundAdapterContext, normalized.entryEndpoint, {
            disableSnapshots: normalized.disableSnapshots === true,
        });
        const inboundStart = Date.now();
        // Phase 0: observe client inbound payload violations (best-effort; no rewrites).
        recordHubPolicyObservation({
            policy: effectivePolicy,
            providerProtocol: this.resolveClientProtocol(normalized.entryEndpoint),
            payload: rawRequest,
            phase: "client_inbound",
            stageRecorder: inboundRecorder,
            requestId: normalized.id,
        });
        const formatEnvelope = await measureHubStage(normalized.id, "req_inbound.stage1_format_parse", () => runReqInboundStage1FormatParse({
            rawRequest,
            adapterContext: inboundAdapterContext,
            stageRecorder: inboundRecorder,
        }));
        const responsesResumeFromMetadata = readResponsesResumeFromMetadata(normalized.metadata);
        const inboundStage2 = await measureHubStage(normalized.id, "req_inbound.stage2_semantic_map", () => runReqInboundStage2SemanticMap({
            adapterContext: inboundAdapterContext,
            formatEnvelope,
            semanticMapper,
            ...(responsesResumeFromMetadata
                ? { responsesResume: responsesResumeFromMetadata }
                : {}),
            stageRecorder: inboundRecorder,
        }));
        // responsesResume must not enter chat_process as metadata; it is lifted into chat.semantics in stage2.
        if (responsesResumeFromMetadata &&
            normalized.metadata &&
            Object.prototype.hasOwnProperty.call(normalized.metadata, "responsesResume")) {
            delete normalized.metadata.responsesResume;
        }
        const contextSnapshot = await measureHubStage(normalized.id, "req_inbound.stage3_context_capture", () => {
            if (inboundStage2.responsesContext) {
                return inboundStage2.responsesContext;
            }
            return hooks.captureContext({
                rawRequest,
                adapterContext: inboundAdapterContext,
                stageRecorder: inboundRecorder,
            });
        });
        let standardizedRequest = applyChatProcessEntryMediaCleanup(inboundStage2.standardizedRequest);
        try {
            const rt = readRuntimeMetadata(normalized.metadata);
            const mode = String(rt?.applyPatchToolMode || "")
                .trim()
                .toLowerCase();
            if (mode === "freeform" || mode === "schema") {
                standardizedRequest.metadata.applyPatchToolMode = mode;
            }
        }
        catch {
            // best-effort: do not block request handling due to metadata propagation failures
        }
        const activeProcessMode = resolveActiveProcessMode(normalized.processMode, standardizedRequest.messages);
        if (activeProcessMode !== normalized.processMode) {
            normalized.processMode = activeProcessMode;
        }
        const passthroughAudit = activeProcessMode === "passthrough"
            ? buildPassthroughAudit(rawRequest, normalized.providerProtocol)
            : undefined;
        const inboundEnd = Date.now();
        const nodeResults = [];
        nodeResults.push({
            id: "req_inbound",
            success: true,
            metadata: {
                node: "req_inbound",
                executionTime: inboundEnd - inboundStart,
                startTime: inboundStart,
                endTime: inboundEnd,
                dataProcessed: {
                    messages: standardizedRequest.messages.length,
                    tools: standardizedRequest.tools?.length ?? 0,
                },
            },
        });
        // 将 VirtualRouter 层的 servertool 相关配置注入到 metadata，保证响应侧
        // servertool（第三跳 reenter）也能访问到相同配置，即使当前 route 标记为 passthrough。
        const metaBase = {
            ...(normalized.metadata ?? {}),
        };
        const rtBase = ensureRuntimeMetadata(metaBase);
        const webSearchConfig = this.config.virtualRouter?.webSearch;
        if (webSearchConfig) {
            rtBase.webSearch = webSearchConfig;
        }
        const execCommandGuard = this.config.virtualRouter?.execCommandGuard;
        if (execCommandGuard) {
            rtBase.execCommandGuard = execCommandGuard;
        }
        const clockConfig = this.config.virtualRouter?.clock;
        if (clockConfig) {
            rtBase.clock = clockConfig;
        }
        normalized.metadata = metaBase;
        let processedRequest;
        if (activeProcessMode !== "passthrough") {
            assertNoMappableSemanticsInMetadata(metaBase, "chat_process.request.entry");
            const processResult = await measureHubStage(normalized.id, "req_process.stage1_tool_governance", () => runReqProcessStage1ToolGovernance({
                request: standardizedRequest,
                rawPayload: rawRequest,
                metadata: metaBase,
                entryEndpoint: normalized.entryEndpoint,
                requestId: normalized.id,
                stageRecorder: inboundRecorder,
            }));
            processedRequest = processResult.processedRequest;
            // Surface request-side clock reservation into pipeline metadata so response conversion
            // can commit delivery only after a successful response is produced.
            try {
                const reservation = processedRequest?.metadata
                    ?.__clockReservation;
                if (reservation && typeof reservation === "object") {
                    metaBase.__clockReservation =
                        reservation;
                }
            }
            catch {
                // best-effort: do not block request handling due to metadata propagation failures
            }
            if (processResult.nodeResult) {
                nodeResults.push(this.convertProcessNodeResult("chat_process.req.stage4.tool_governance", processResult.nodeResult));
            }
        }
        else {
            nodeResults.push({
                id: "chat_process.req.stage4.tool_governance",
                success: true,
                metadata: {
                    node: "chat_process.req.stage4.tool_governance",
                    skipped: true,
                    reason: "process_mode_passthrough_parse_record_only",
                },
            });
            if (passthroughAudit) {
                annotatePassthroughGovernanceSkip(passthroughAudit);
            }
        }
        let workingRequest = syncResponsesContextFromCanonicalMessages(processedRequest ?? standardizedRequest);
        // 使用与 VirtualRouter 一致的 tiktoken 计数逻辑，对标准化请求进行一次
        // 上下文 token 估算，供后续 usage 归一化与统计使用。
        try {
            const estimatedTokens = estimateSessionBoundTokens(workingRequest, normalized.metadata) ?? computeRequestTokens(workingRequest, "");
            if (typeof estimatedTokens === "number" &&
                Number.isFinite(estimatedTokens) &&
                estimatedTokens > 0) {
                normalized.metadata = normalized.metadata || {};
                normalized.metadata.estimatedInputTokens =
                    estimatedTokens;
            }
        }
        catch {
            // 估算失败不应影响主流程
        }
        const normalizedMeta = normalized.metadata;
        // responsesResume is a client-protocol semantic (/v1/responses tool loop) and must live in chat.semantics.
        // Do not read it from metadata once entering chat_process.
        const responsesResume = readResponsesResumeFromRequestSemantics(workingRequest);
        const stdMetadata = workingRequest?.metadata;
        const hasImageAttachment = containsImageAttachment((workingRequest.messages ?? []));
        const serverToolRequired = stdMetadata?.webSearchEnabled === true ||
            stdMetadata?.serverToolRequired === true;
        const sessionIdentifiers = extractSessionIdentifiersFromMetadata(normalized.metadata);
        // 将从 metadata / clientHeaders 中解析出的会话标识同步回 normalized.metadata，
        // 便于后续 AdapterContext（响应侧 servertool）也能访问到相同的 sessionId /
        // conversationId，用于 sticky-session 相关逻辑（例如 stopMessage）。
        if (sessionIdentifiers.sessionId &&
            normalized.metadata &&
            typeof normalized.metadata === "object") {
            normalized.metadata.sessionId =
                sessionIdentifiers.sessionId;
        }
        if (sessionIdentifiers.conversationId &&
            normalized.metadata &&
            typeof normalized.metadata === "object") {
            normalized.metadata.conversationId =
                sessionIdentifiers.conversationId;
        }
        const disableStickyRoutes = readRuntimeMetadata(normalized.metadata)?.disableStickyRoutes === true;
        const stopMessageRouterMetadata = resolveStopMessageRouterMetadata(normalized.metadata);
        const estimatedInputTokens = (() => {
            const value = normalized.metadata
                ?.estimatedInputTokens;
            return typeof value === "number" && Number.isFinite(value)
                ? value
                : undefined;
        })();
        const metadataInput = {
            requestId: normalized.id,
            entryEndpoint: normalized.entryEndpoint,
            processMode: normalized.processMode,
            stream: normalized.stream,
            direction: normalized.direction,
            providerProtocol: normalized.providerProtocol,
            routeHint: normalized.routeHint,
            stage: normalized.stage,
            responsesResume: responsesResume,
            ...(estimatedInputTokens !== undefined ? { estimatedInputTokens } : {}),
            ...(disableStickyRoutes ? { disableStickyRoutes: true } : {}),
            ...(serverToolRequired ? { serverToolRequired: true } : {}),
            ...(sessionIdentifiers.sessionId
                ? { sessionId: sessionIdentifiers.sessionId }
                : {}),
            ...(sessionIdentifiers.conversationId
                ? { conversationId: sessionIdentifiers.conversationId }
                : {}),
            ...stopMessageRouterMetadata,
        };
        logHubStageTiming(normalized.id, "req_process.stage2_route_select", "start");
        const routing = runReqProcessStage2RouteSelect({
            routerEngine: this.routerEngine,
            request: workingRequest,
            metadataInput,
            normalizedMetadata: normalized.metadata,
            stageRecorder: inboundRecorder,
        });
        logHubStageTiming(normalized.id, "req_process.stage2_route_select", "completed");
        // Emit virtual router hit log for debugging (orange [virtual-router] ...)
        try {
            const routeName = routing.decision?.routeName;
            const providerKey = routing.target?.providerKey;
            const modelId = workingRequest.model;
            const logger = (normalized.metadata &&
                normalized.metadata.logger);
            if (logger &&
                typeof logger.logVirtualRouterHit === "function" &&
                routeName &&
                providerKey) {
                logger.logVirtualRouterHit(routeName, providerKey, typeof modelId === "string" ? modelId : undefined);
            }
        }
        catch {
            // logging must not break routing
        }
        const outboundStream = this.resolveOutboundStreamIntent(routing.target?.streaming);
        workingRequest = this.applyOutboundStreamPreference(workingRequest, outboundStream, activeProcessMode);
        this.applyMaxTokensPolicy(workingRequest, routing.target);
        const outboundAdapterContext = this.buildAdapterContext(normalized, routing.target);
        if (routing.target?.compatibilityProfile) {
            outboundAdapterContext.compatibilityProfile =
                routing.target.compatibilityProfile;
        }
        const outboundProtocol = outboundAdapterContext.providerProtocol;
        if (activeProcessMode === "passthrough" &&
            outboundProtocol !== normalized.providerProtocol) {
            throw new Error(`[HubPipeline] passthrough requires matching protocols: entry=${normalized.providerProtocol}, target=${outboundProtocol}`);
        }
        // Snapshots must be grouped by entry endpoint (client-facing protocol), not by provider protocol.
        // Otherwise one request would be split across multiple folders (e.g. openai-responses + anthropic-messages),
        // which breaks codex-samples correlation.
        const outboundRecorder = this.maybeCreateStageRecorder(outboundAdapterContext, normalized.entryEndpoint, {
            disableSnapshots: normalized.disableSnapshots === true,
        });
        const outboundStart = Date.now();
        let providerPayload;
        let shadowBaselineProviderPayload;
        if (activeProcessMode === "passthrough") {
            providerPayload = jsonClone(rawRequest);
            if (typeof outboundStream === "boolean") {
                providerPayload.stream = outboundStream;
            }
            if (passthroughAudit) {
                attachPassthroughProviderInputAudit(passthroughAudit, providerPayload, outboundProtocol);
            }
        }
        else {
            const protocolSwitch = outboundProtocol !== normalized.providerProtocol;
            const outboundHooks = protocolSwitch
                ? this.resolveProtocolHooks(outboundProtocol)
                : hooks;
            if (!outboundHooks) {
                throw new Error(`[HubPipeline] Unsupported provider protocol for hub pipeline: ${outboundProtocol}`);
            }
            const outboundSemanticMapper = protocolSwitch
                ? outboundHooks.createSemanticMapper()
                : semanticMapper;
            const outboundContextMetadataKey = protocolSwitch
                ? outboundHooks.contextMetadataKey
                : hooks.contextMetadataKey;
            const outboundContextSnapshot = protocolSwitch
                ? undefined
                : contextSnapshot;
            const outboundStage1 = await measureHubStage(normalized.id, "req_outbound.stage1_semantic_map", () => runReqOutboundStage1SemanticMap({
                request: workingRequest,
                adapterContext: outboundAdapterContext,
                semanticMapper: outboundSemanticMapper,
                contextSnapshot: outboundContextSnapshot,
                contextMetadataKey: outboundContextMetadataKey,
                stageRecorder: outboundRecorder,
            }));
            let formattedPayload = await measureHubStage(normalized.id, "req_outbound.stage2_format_build", () => runReqOutboundStage2FormatBuild({
                formatEnvelope: outboundStage1.formatEnvelope,
                stageRecorder: outboundRecorder,
            }));
            formattedPayload = await measureHubStage(normalized.id, "req_outbound.stage3_compat", () => runReqOutboundStage3Compat({
                payload: formattedPayload,
                adapterContext: outboundAdapterContext,
                stageRecorder: outboundRecorder,
            }));
            if (shadowCompareBaselineMode) {
                const baselinePolicy = {
                    ...(effectivePolicy ?? {}),
                    mode: shadowCompareBaselineMode,
                };
                // Compute a baseline provider payload in the *same execution*, without recording
                // snapshots/diffs and without re-running the full pipeline. This avoids side effects
                // (conversation store, followup captures, etc.) that a second execute() would trigger.
                const baselineFormatted = typeof globalThis.structuredClone === "function"
                    ? globalThis.structuredClone(formattedPayload)
                    : jsonClone(formattedPayload);
                let baselinePayload = applyHubProviderOutboundPolicy({
                    policy: baselinePolicy,
                    providerProtocol: outboundProtocol,
                    compatibilityProfile: typeof outboundAdapterContext.compatibilityProfile === "string"
                        ? outboundAdapterContext.compatibilityProfile
                        : undefined,
                    payload: baselineFormatted,
                    stageRecorder: undefined,
                    requestId: normalized.id,
                });
                baselinePayload = applyProviderOutboundToolSurface({
                    config: this.config.toolSurface,
                    providerProtocol: outboundProtocol,
                    payload: baselinePayload,
                    stageRecorder: undefined,
                    requestId: normalized.id,
                });
                shadowBaselineProviderPayload = baselinePayload;
            }
            // Phase 0/1: observe provider outbound payload violations before any enforcement rewrites.
            // This provides black-box visibility into what the pipeline would have sent upstream.
            recordHubPolicyObservation({
                policy: effectivePolicy,
                providerProtocol: outboundProtocol,
                compatibilityProfile: typeof outboundAdapterContext.compatibilityProfile === "string"
                    ? outboundAdapterContext.compatibilityProfile
                    : undefined,
                payload: formattedPayload,
                stageRecorder: outboundRecorder,
                requestId: normalized.id,
            });
            providerPayload = applyHubProviderOutboundPolicy({
                policy: effectivePolicy,
                providerProtocol: outboundProtocol,
                compatibilityProfile: typeof outboundAdapterContext.compatibilityProfile === "string"
                    ? outboundAdapterContext.compatibilityProfile
                    : undefined,
                payload: formattedPayload,
                stageRecorder: outboundRecorder,
                requestId: normalized.id,
            });
            providerPayload = applyProviderOutboundToolSurface({
                config: this.config.toolSurface,
                providerProtocol: outboundProtocol,
                payload: providerPayload,
                stageRecorder: outboundRecorder,
                requestId: normalized.id,
            });
            providerPayload = maybeApplyDirectBuiltinWebSearchTool(providerPayload, outboundAdapterContext, outboundProtocol);
            recordHubPolicyObservation({
                policy: effectivePolicy,
                providerProtocol: outboundProtocol,
                compatibilityProfile: typeof outboundAdapterContext.compatibilityProfile === "string"
                    ? outboundAdapterContext.compatibilityProfile
                    : undefined,
                payload: providerPayload,
                stageRecorder: outboundRecorder,
                requestId: normalized.id,
            });
            if (passthroughAudit) {
                attachPassthroughProviderInputAudit(passthroughAudit, providerPayload, outboundProtocol);
            }
        }
        const outboundEnd = Date.now();
        nodeResults.push({
            id: "req_outbound",
            success: true,
            metadata: {
                node: "req_outbound",
                executionTime: outboundEnd - outboundStart,
                startTime: outboundStart,
                endTime: outboundEnd,
                dataProcessed: {
                    messages: workingRequest.messages.length,
                    tools: workingRequest.tools?.length ?? 0,
                },
            },
        });
        // 为响应侧 servertool/web_search 提供一次性 Chat 请求快照，便于在 Hub 内部实现
        // 第三跳（将工具结果注入消息历史后重新调用主模型）。
        //
        // 注意：这里不再根据 processMode(passthrough/chat) 做分支判断——即使某些
        // route 将 processMode 标记为 passthrough，我们仍然需要保留一次规范化后的
        // Chat 请求快照，供 stopMessage 等被动触发型
        // servertool 在响应阶段使用。
        //
        // 之前这里通过 JSON.stringify/parse 做深拷贝，但在部分 Responses/Gemini
        // 场景下，workingRequest 上携带的 metadata 可能包含无法安全序列化的字段，
        // 导致克隆过程抛错、capturedChatRequest 被静默丢弃，从而让响应侧的
        // stop_message_auto 等 ServerTool 无法获取上一跳的 Chat 请求。
        //
        // 对于 capturedChatRequest，我们只需要一个“可读快照”，不会在后续流程中
        // 对其做就地修改，因此可以直接使用浅拷贝结构，避免序列化失败导致整段
        // 逻辑失效。
        // Deep-clone a JSON-safe snapshot for servertool followups.
        // Only capture the canonical Chat payload fields (model/messages/tools/parameters) to keep it serializable.
        const capturedChatRequest = {
            model: workingRequest.model,
            messages: jsonClone(workingRequest.messages),
            tools: workingRequest.tools
                ? jsonClone(workingRequest.tools)
                : workingRequest.tools,
            parameters: workingRequest.parameters
                ? jsonClone(workingRequest.parameters)
                : workingRequest.parameters,
        };
        const metadata = {
            ...normalized.metadata,
            capturedChatRequest,
            entryEndpoint: normalized.entryEndpoint,
            providerProtocol: outboundProtocol,
            stream: normalized.stream,
            processMode: normalized.processMode,
            ...(passthroughAudit ? { passthroughAudit } : {}),
            routeHint: normalized.routeHint,
            target: routing.target,
            ...(typeof outboundStream === "boolean"
                ? { providerStream: outboundStream }
                : {}),
            ...(shadowBaselineProviderPayload
                ? {
                    hubShadowCompare: {
                        baselineMode: shadowCompareBaselineMode,
                        candidateMode: (effectivePolicy?.mode ?? "off"),
                        providerProtocol: outboundProtocol,
                        baselineProviderPayload: shadowBaselineProviderPayload,
                    },
                }
                : {}),
        };
        if (hasImageAttachment) {
            metadata.hasImageAttachment = true;
        }
        else {
            delete metadata.hasImageAttachment;
        }
        return {
            requestId: normalized.id,
            providerPayload,
            standardizedRequest,
            processedRequest,
            routingDecision: routing.decision,
            routingDiagnostics: routing.diagnostics,
            target: routing.target,
            metadata,
            nodeResults,
        };
    }
    resolveClientProtocol(entryEndpoint) {
        const protocol = resolveHubClientProtocolWithNative(entryEndpoint);
        if (protocol === "openai-responses" ||
            protocol === "anthropic-messages" ||
            protocol === "openai-chat") {
            return protocol;
        }
        return "openai-chat";
    }
    coerceStandardizedRequestFromPayload(payload, normalized) {
        const model = typeof payload.model === "string" && payload.model.trim().length
            ? payload.model.trim()
            : "";
        if (!model) {
            throw new Error("[HubPipeline] outbound stage requires payload.model");
        }
        const messages = Array.isArray(payload.messages)
            ? payload.messages
            : null;
        if (!messages) {
            throw new Error("[HubPipeline] outbound stage requires payload.messages[]");
        }
        const tools = Array.isArray(payload.tools)
            ? payload.tools
            : undefined;
        const parameters = payload.parameters &&
            typeof payload.parameters === "object" &&
            !Array.isArray(payload.parameters)
            ? payload.parameters
            : {};
        const semanticsFromPayload = payload.semantics &&
            typeof payload.semantics === "object" &&
            !Array.isArray(payload.semantics)
            ? jsonClone(payload.semantics)
            : undefined;
        const metadataFromPayload = payload.metadata &&
            typeof payload.metadata === "object" &&
            !Array.isArray(payload.metadata)
            ? payload.metadata
            : undefined;
        const standardizedRequest = {
            model,
            messages,
            ...(tools ? { tools } : {}),
            parameters,
            metadata: {
                originalEndpoint: normalized.entryEndpoint,
                ...(metadataFromPayload ? metadataFromPayload : {}),
                requestId: normalized.id,
                stream: normalized.stream,
                processMode: normalized.processMode,
                ...(normalized.routeHint ? { routeHint: normalized.routeHint } : {}),
            },
            ...(semanticsFromPayload
                ? { semantics: semanticsFromPayload }
                : {}),
        };
        // Ensure followup/chat_process entry can still preserve mappable semantics
        // without injecting them into metadata.
        try {
            const semantics = standardizedRequest.semantics &&
                typeof standardizedRequest.semantics === "object"
                ? standardizedRequest.semantics
                : (standardizedRequest.semantics = {});
            if (!semantics.tools ||
                typeof semantics.tools !== "object" ||
                Array.isArray(semantics.tools)) {
                semantics.tools = {};
            }
            const toolsNode = semantics.tools;
            if (Array.isArray(payload.tools) &&
                payload.tools.length &&
                toolsNode.clientToolsRaw === undefined) {
                toolsNode.clientToolsRaw = jsonClone(payload.tools);
            }
        }
        catch {
            // best-effort
        }
        // Keep rawPayload minimal and JSON-safe; chat-process only needs the OpenAI-chat-like surface here.
        const rawPayload = {
            model,
            messages,
            ...(tools ? { tools } : {}),
            ...(parameters && Object.keys(parameters).length ? { parameters } : {}),
        };
        return { standardizedRequest, rawPayload };
    }
    async executeChatProcessEntryPipeline(normalized) {
        const hooks = this.resolveProtocolHooks(normalized.providerProtocol);
        if (!hooks) {
            throw new Error(`Unsupported provider protocol for hub pipeline: ${normalized.providerProtocol}`);
        }
        const nodeResults = [];
        nodeResults.push({
            id: "req_inbound",
            success: true,
            metadata: {
                node: "req_inbound",
                skipped: true,
                reason: "stage=outbound",
                dataProcessed: {},
            },
        });
        const rawPayloadInput = this.asJsonObject(normalized.payload);
        const { standardizedRequest: standardizedRequestBase, rawPayload } = this.coerceStandardizedRequestFromPayload(rawPayloadInput, normalized);
        // Keep metadata injection consistent with the inbound path: servertool/web_search config must be available
        // to chat-process/tool governance even when request enters at outbound stage.
        const metaBase = {
            ...(normalized.metadata ?? {}),
        };
        const rtBase = ensureRuntimeMetadata(metaBase);
        const webSearchConfig = this.config.virtualRouter?.webSearch;
        if (webSearchConfig) {
            rtBase.webSearch = webSearchConfig;
        }
        const execCommandGuard = this.config.virtualRouter?.execCommandGuard;
        if (execCommandGuard) {
            rtBase.execCommandGuard = execCommandGuard;
        }
        const clockConfig = this.config.virtualRouter?.clock;
        if (clockConfig) {
            rtBase.clock = clockConfig;
        }
        normalized.metadata = metaBase;
        const cleanedRequest = applyChatProcessEntryMediaCleanup(standardizedRequestBase);
        let standardizedRequest = cleanedRequest;
        const activeProcessMode = resolveActiveProcessMode(normalized.processMode, cleanedRequest.messages);
        if (activeProcessMode !== normalized.processMode) {
            normalized.processMode = activeProcessMode;
        }
        const passthroughAudit = activeProcessMode === "passthrough"
            ? buildPassthroughAudit(rawPayload, normalized.providerProtocol)
            : undefined;
        // Semantic Gate (chat_process entry): lift any mappable protocol semantics from metadata into request.semantics.
        // This is the last chance before entering chat_process; after this point we fail-fast on banned metadata keys.
        try {
            standardizedRequest = liftResponsesResumeIntoSemantics(standardizedRequest, metaBase);
        }
        catch {
            // best-effort; validation happens below
        }
        try {
            const rt = readRuntimeMetadata(metaBase);
            const mode = String(rt?.applyPatchToolMode || "")
                .trim()
                .toLowerCase();
            if (mode === "freeform" || mode === "schema") {
                standardizedRequest.metadata.applyPatchToolMode = mode;
            }
        }
        catch {
            // ignore
        }
        const adapterContext = this.buildAdapterContext(normalized);
        const stageRecorder = this.maybeCreateStageRecorder(adapterContext, normalized.entryEndpoint, {
            disableSnapshots: normalized.disableSnapshots === true,
        });
        let processedRequest;
        if (activeProcessMode !== "passthrough") {
            assertNoMappableSemanticsInMetadata(metaBase, "chat_process.request.entry");
            const processResult = await runReqProcessStage1ToolGovernance({
                request: standardizedRequest,
                rawPayload,
                metadata: metaBase,
                entryEndpoint: normalized.entryEndpoint,
                requestId: normalized.id,
                stageRecorder,
            });
            processedRequest = processResult.processedRequest;
            // Surface request-side clock reservation into pipeline metadata so response conversion
            // can commit delivery only after a successful response is produced.
            try {
                const reservation = processedRequest?.metadata
                    ?.__clockReservation;
                if (reservation && typeof reservation === "object") {
                    metaBase.__clockReservation =
                        reservation;
                }
            }
            catch {
                // best-effort
            }
            if (processResult.nodeResult) {
                nodeResults.push(this.convertProcessNodeResult("chat_process.req.stage4.tool_governance", processResult.nodeResult));
            }
        }
        else {
            nodeResults.push({
                id: "chat_process.req.stage4.tool_governance",
                success: true,
                metadata: {
                    node: "chat_process.req.stage4.tool_governance",
                    skipped: true,
                    reason: "process_mode_passthrough_parse_record_only",
                },
            });
            if (passthroughAudit) {
                annotatePassthroughGovernanceSkip(passthroughAudit);
            }
        }
        let workingRequest = syncResponsesContextFromCanonicalMessages(processedRequest ?? standardizedRequest);
        // Token estimate for stats/diagnostics (best-effort).
        try {
            const estimatedTokens = estimateSessionBoundTokens(workingRequest, normalized.metadata) ?? computeRequestTokens(workingRequest, "");
            if (typeof estimatedTokens === "number" &&
                Number.isFinite(estimatedTokens) &&
                estimatedTokens > 0) {
                normalized.metadata = normalized.metadata || {};
                normalized.metadata.estimatedInputTokens =
                    estimatedTokens;
            }
        }
        catch {
            // ignore
        }
        const normalizedMeta = normalized.metadata;
        // responsesResume is a client-protocol semantic (/v1/responses tool loop) and must live in chat.semantics.
        // Do not read it from metadata once entering chat_process.
        const responsesResume = readResponsesResumeFromRequestSemantics(workingRequest);
        const stdMetadata = workingRequest?.metadata;
        const hasImageAttachment = containsImageAttachment((workingRequest.messages ?? []));
        const serverToolRequired = stdMetadata?.webSearchEnabled === true ||
            stdMetadata?.serverToolRequired === true;
        const sessionIdentifiers = extractSessionIdentifiersFromMetadata(normalized.metadata);
        if (sessionIdentifiers.sessionId &&
            normalized.metadata &&
            typeof normalized.metadata === "object") {
            normalized.metadata.sessionId =
                sessionIdentifiers.sessionId;
        }
        if (sessionIdentifiers.conversationId &&
            normalized.metadata &&
            typeof normalized.metadata === "object") {
            normalized.metadata.conversationId =
                sessionIdentifiers.conversationId;
        }
        const disableStickyRoutes = readRuntimeMetadata(normalized.metadata)?.disableStickyRoutes === true;
        const stopMessageRouterMetadata = resolveStopMessageRouterMetadata(normalized.metadata);
        const metadataInput = {
            requestId: normalized.id,
            entryEndpoint: normalized.entryEndpoint,
            processMode: normalized.processMode,
            stream: normalized.stream,
            direction: normalized.direction,
            providerProtocol: normalized.providerProtocol,
            routeHint: normalized.routeHint,
            stage: normalized.stage,
            responsesResume: responsesResume,
            ...(disableStickyRoutes ? { disableStickyRoutes: true } : {}),
            ...(serverToolRequired ? { serverToolRequired: true } : {}),
            ...(sessionIdentifiers.sessionId
                ? { sessionId: sessionIdentifiers.sessionId }
                : {}),
            ...(sessionIdentifiers.conversationId
                ? { conversationId: sessionIdentifiers.conversationId }
                : {}),
            ...stopMessageRouterMetadata,
        };
        const routing = runReqProcessStage2RouteSelect({
            routerEngine: this.routerEngine,
            request: workingRequest,
            metadataInput,
            normalizedMetadata: normalized.metadata,
            stageRecorder,
        });
        // Emit virtual router hit log for debugging (same as inbound path).
        try {
            const routeName = routing.decision?.routeName;
            const providerKey = routing.target?.providerKey;
            const modelId = workingRequest.model;
            const logger = (normalized.metadata &&
                normalized.metadata.logger);
            if (logger &&
                typeof logger.logVirtualRouterHit === "function" &&
                routeName &&
                providerKey) {
                logger.logVirtualRouterHit(routeName, providerKey, typeof modelId === "string" ? modelId : undefined);
            }
        }
        catch {
            // ignore
        }
        const outboundStream = this.resolveOutboundStreamIntent(routing.target?.streaming);
        workingRequest = this.applyOutboundStreamPreference(workingRequest, outboundStream, activeProcessMode);
        const outboundAdapterContext = this.buildAdapterContext(normalized, routing.target);
        if (routing.target?.compatibilityProfile) {
            outboundAdapterContext.compatibilityProfile =
                routing.target.compatibilityProfile;
        }
        const outboundProtocol = outboundAdapterContext.providerProtocol;
        if (activeProcessMode === "passthrough" &&
            outboundProtocol !== normalized.providerProtocol) {
            throw new Error(`[HubPipeline] passthrough requires matching protocols: entry=${normalized.providerProtocol}, target=${outboundProtocol}`);
        }
        const outboundRecorder = this.maybeCreateStageRecorder(outboundAdapterContext, normalized.entryEndpoint, {
            disableSnapshots: normalized.disableSnapshots === true,
        });
        const outboundStart = Date.now();
        let providerPayload;
        if (activeProcessMode === "passthrough") {
            providerPayload = jsonClone(rawPayloadInput);
            if (typeof outboundStream === "boolean") {
                providerPayload.stream = outboundStream;
            }
            if (passthroughAudit) {
                attachPassthroughProviderInputAudit(passthroughAudit, providerPayload, outboundProtocol);
            }
        }
        else {
            const protocolSwitch = outboundProtocol !== normalized.providerProtocol;
            const outboundHooks = protocolSwitch
                ? this.resolveProtocolHooks(outboundProtocol)
                : hooks;
            if (!outboundHooks) {
                throw new Error(`[HubPipeline] Unsupported provider protocol for hub pipeline: ${outboundProtocol}`);
            }
            const outboundSemanticMapper = protocolSwitch
                ? outboundHooks.createSemanticMapper()
                : hooks.createSemanticMapper();
            const outboundContextMetadataKey = protocolSwitch
                ? outboundHooks.contextMetadataKey
                : hooks.contextMetadataKey;
            const outboundContextSnapshot = undefined;
            const outboundStage1 = await measureHubStage(normalized.id, "req_outbound.stage1_semantic_map", () => runReqOutboundStage1SemanticMap({
                request: workingRequest,
                adapterContext: outboundAdapterContext,
                semanticMapper: outboundSemanticMapper,
                contextSnapshot: outboundContextSnapshot,
                contextMetadataKey: outboundContextMetadataKey,
                stageRecorder: outboundRecorder,
            }));
            let formattedPayload = await measureHubStage(normalized.id, "req_outbound.stage2_format_build", () => runReqOutboundStage2FormatBuild({
                formatEnvelope: outboundStage1.formatEnvelope,
                stageRecorder: outboundRecorder,
            }));
            formattedPayload = await measureHubStage(normalized.id, "req_outbound.stage3_compat", () => runReqOutboundStage3Compat({
                payload: formattedPayload,
                adapterContext: outboundAdapterContext,
                stageRecorder: outboundRecorder,
            }));
            // Phase 0/1: observe + enforce provider outbound policy and tool surface (same as inbound path).
            const effectivePolicy = normalized.policyOverride ?? this.config.policy;
            recordHubPolicyObservation({
                policy: effectivePolicy,
                providerProtocol: outboundProtocol,
                compatibilityProfile: typeof outboundAdapterContext.compatibilityProfile === "string"
                    ? outboundAdapterContext.compatibilityProfile
                    : undefined,
                payload: formattedPayload,
                stageRecorder: outboundRecorder,
                requestId: normalized.id,
            });
            providerPayload = applyHubProviderOutboundPolicy({
                policy: effectivePolicy,
                providerProtocol: outboundProtocol,
                compatibilityProfile: typeof outboundAdapterContext.compatibilityProfile === "string"
                    ? outboundAdapterContext.compatibilityProfile
                    : undefined,
                payload: formattedPayload,
                stageRecorder: outboundRecorder,
                requestId: normalized.id,
            });
            providerPayload = applyProviderOutboundToolSurface({
                config: this.config.toolSurface,
                providerProtocol: outboundProtocol,
                payload: providerPayload,
                stageRecorder: outboundRecorder,
                requestId: normalized.id,
            });
            providerPayload = maybeApplyDirectBuiltinWebSearchTool(providerPayload, outboundAdapterContext, outboundProtocol);
            recordHubPolicyObservation({
                policy: effectivePolicy,
                providerProtocol: outboundProtocol,
                compatibilityProfile: typeof outboundAdapterContext.compatibilityProfile === "string"
                    ? outboundAdapterContext.compatibilityProfile
                    : undefined,
                payload: providerPayload,
                stageRecorder: outboundRecorder,
                requestId: normalized.id,
            });
            if (passthroughAudit) {
                attachPassthroughProviderInputAudit(passthroughAudit, providerPayload, outboundProtocol);
            }
        }
        const outboundEnd = Date.now();
        nodeResults.push({
            id: "req_outbound",
            success: true,
            metadata: {
                node: "req_outbound",
                executionTime: outboundEnd - outboundStart,
                startTime: outboundStart,
                endTime: outboundEnd,
                dataProcessed: {
                    messages: workingRequest.messages.length,
                    tools: workingRequest.tools?.length ?? 0,
                },
            },
        });
        const capturedChatRequest = {
            model: workingRequest.model,
            messages: jsonClone(workingRequest.messages),
            tools: workingRequest.tools
                ? jsonClone(workingRequest.tools)
                : workingRequest.tools,
            parameters: workingRequest.parameters
                ? jsonClone(workingRequest.parameters)
                : workingRequest.parameters,
        };
        const metadata = {
            ...normalized.metadata,
            capturedChatRequest,
            entryEndpoint: normalized.entryEndpoint,
            providerProtocol: outboundProtocol,
            stream: normalized.stream,
            processMode: normalized.processMode,
            ...(passthroughAudit ? { passthroughAudit } : {}),
            routeHint: normalized.routeHint,
            target: routing.target,
            ...(typeof outboundStream === "boolean"
                ? { providerStream: outboundStream }
                : {}),
        };
        if (hasImageAttachment) {
            metadata.hasImageAttachment = true;
        }
        else {
            delete metadata.hasImageAttachment;
        }
        return {
            requestId: normalized.id,
            providerPayload,
            standardizedRequest,
            processedRequest,
            routingDecision: routing.decision,
            routingDiagnostics: routing.diagnostics,
            target: routing.target,
            metadata,
            nodeResults,
        };
    }
    async execute(request) {
        const normalized = await this.normalizeRequest(request);
        if (normalized.direction === "request" &&
            normalized.hubEntryMode === "chat_process") {
            return await this.executeChatProcessEntryPipeline(normalized);
        }
        const hooks = this.resolveProtocolHooks(normalized.providerProtocol);
        if (!hooks) {
            throw new Error(`Unsupported provider protocol for hub pipeline: ${normalized.providerProtocol}`);
        }
        return await this.executeRequestStagePipeline(normalized, hooks);
    }
    captureAnthropicAliasMap(normalized, adapterContext, chatEnvelope) {
        if (!this.shouldCaptureAnthropicAlias(normalized.entryEndpoint)) {
            return;
        }
        const aliasMap = this.resolveAliasMapFromSources(adapterContext, chatEnvelope);
        if (!aliasMap) {
            return;
        }
        // A1: tool name alias map is mappable semantics and must live in chat.semantics (never metadata).
        try {
            if (!chatEnvelope.semantics ||
                typeof chatEnvelope.semantics !== "object" ||
                Array.isArray(chatEnvelope.semantics)) {
                chatEnvelope.semantics = {};
            }
            const semantics = chatEnvelope.semantics;
            if (!semantics.tools || !isJsonObject(semantics.tools)) {
                semantics.tools = {};
            }
            const toolsNode = semantics.tools;
            if (!isJsonObject(toolsNode.toolNameAliasMap) &&
                !isJsonObject(toolsNode.toolAliasMap)) {
                toolsNode.toolNameAliasMap = jsonClone(aliasMap);
            }
        }
        catch {
            // best-effort: never block request handling due to alias map propagation failures
        }
    }
    shouldCaptureAnthropicAlias(endpoint) {
        return (typeof endpoint === "string" &&
            endpoint.toLowerCase().includes("/v1/messages"));
    }
    resolveAliasMapFromSources(adapterContext, chatEnvelope) {
        const fromContext = coerceAliasMap(adapterContext.anthropicToolNameMap);
        if (fromContext) {
            return fromContext;
        }
        const metadataNode = chatEnvelope.metadata;
        const direct = metadataNode
            ? coerceAliasMap(metadataNode.anthropicToolNameMap)
            : undefined;
        if (direct) {
            return direct;
        }
        const contextNode = metadataNode &&
            metadataNode.context &&
            typeof metadataNode.context === "object"
            ? metadataNode.context
            : undefined;
        const fromContextNode = coerceAliasMap(contextNode?.anthropicToolNameMap);
        if (fromContextNode) {
            return fromContextNode;
        }
        return readAliasMapFromSemantics(chatEnvelope);
    }
    resolveProtocolHooks(protocol) {
        switch (protocol) {
            case "openai-chat":
                return {
                    createFormatAdapter: () => new ChatFormatAdapter(),
                    createSemanticMapper: () => new ChatSemanticMapper(),
                    captureContext: (options) => runChatContextCapture(options),
                    contextMetadataKey: "chatContext",
                };
            case "openai-responses":
                return {
                    createFormatAdapter: () => new ResponsesFormatAdapter(),
                    createSemanticMapper: () => new ResponsesSemanticMapper(),
                    captureContext: createResponsesContextCapture(captureResponsesContextSnapshot),
                    contextMetadataKey: "responsesContext",
                };
            case "anthropic-messages":
                return {
                    createFormatAdapter: () => new AnthropicFormatAdapter(),
                    createSemanticMapper: () => new AnthropicSemanticMapper(),
                    captureContext: (options) => runChatContextCapture(options),
                    contextMetadataKey: "anthropicContext",
                };
            case "gemini-chat":
                return {
                    createFormatAdapter: () => new GeminiFormatAdapter(),
                    createSemanticMapper: () => new GeminiSemanticMapper(),
                    captureContext: createNoopContextCapture("gemini-chat"),
                };
            default:
                return undefined;
        }
    }
    buildAdapterContext(normalized, target) {
        const metadata = normalized.metadata || {};
        const providerProtocol = target?.outboundProfile ||
            normalized.providerProtocol;
        const providerId = (target?.providerKey || metadata.providerKey);
        const routeId = metadata.routeName;
        const profileId = (target?.providerKey || metadata.pipelineId);
        const targetCompatProfile = typeof target?.compatibilityProfile === "string" &&
            target.compatibilityProfile.trim()
            ? target.compatibilityProfile.trim()
            : undefined;
        const metadataCompatProfile = typeof metadata.compatibilityProfile ===
            "string"
            ? String(metadata.compatibilityProfile).trim()
            : undefined;
        // When routing has already selected a target runtime, compat must be target-scoped only.
        // Never inherit stale top-level metadata.compatibilityProfile from a previous hop.
        const compatibilityProfile = target
            ? targetCompatProfile
            : metadataCompatProfile;
        const streamingHint = normalized.stream === true
            ? "force"
            : normalized.stream === false
                ? "disable"
                : "auto";
        const toolCallIdStyle = normalizeReqInboundToolCallIdStyleWithNative(metadata.toolCallIdStyle);
        const adapterContext = {
            requestId: normalized.id,
            entryEndpoint: normalized.entryEndpoint || "/v1/chat/completions",
            providerProtocol,
            providerId,
            routeId,
            profileId,
            streamingHint,
            toolCallIdStyle,
            ...(compatibilityProfile ? { compatibilityProfile } : {}),
        };
        const targetDeepseek = isJsonObject(target?.deepseek)
            ? jsonClone(target.deepseek)
            : undefined;
        if (targetDeepseek) {
            adapterContext.deepseek = targetDeepseek;
            const rtCarrier = isJsonObject(adapterContext.__rt)
                ? {
                    ...adapterContext.__rt,
                }
                : {};
            rtCarrier.deepseek = targetDeepseek;
            adapterContext.__rt =
                rtCarrier;
        }
        const runtime = metadata.runtime;
        if (runtime && typeof runtime === "object" && !Array.isArray(runtime)) {
            adapterContext.runtime = jsonClone(runtime);
        }
        const clientRequestId = typeof metadata.clientRequestId === "string"
            ? metadata.clientRequestId.trim()
            : "";
        if (clientRequestId) {
            adapterContext.clientRequestId =
                clientRequestId;
        }
        const groupRequestId = typeof metadata.groupRequestId === "string"
            ? metadata.groupRequestId.trim()
            : "";
        if (groupRequestId) {
            adapterContext.groupRequestId =
                groupRequestId;
        }
        if (typeof metadata.originalModelId === "string") {
            adapterContext.originalModelId = metadata.originalModelId;
        }
        if (typeof metadata.clientModelId === "string") {
            adapterContext.clientModelId = metadata.clientModelId;
        }
        if (typeof metadata.assignedModelId === "string") {
            adapterContext.modelId =
                metadata.assignedModelId;
        }
        const estimatedInputTokens = Number(metadata.estimatedInputTokens ??
            metadata.estimated_tokens ??
            metadata.estimatedTokens);
        if (Number.isFinite(estimatedInputTokens) && estimatedInputTokens > 0) {
            adapterContext.estimatedInputTokens =
                Math.max(1, Math.round(estimatedInputTokens));
        }
        const rt = cloneRuntimeMetadata(metadata);
        if (rt) {
            adapterContext.__rt = rt;
        }
        const capturedChatRequest = metadata.capturedChatRequest &&
            typeof metadata.capturedChatRequest ===
                "object" &&
            !Array.isArray(metadata.capturedChatRequest)
            ? jsonClone(metadata
                .capturedChatRequest)
            : undefined;
        if (capturedChatRequest) {
            adapterContext.capturedChatRequest =
                capturedChatRequest;
        }
        const sessionId = typeof metadata.sessionId === "string"
            ? metadata.sessionId.trim()
            : "";
        if (sessionId) {
            adapterContext.sessionId = sessionId;
        }
        const conversationId = typeof metadata.conversationId === "string"
            ? metadata.conversationId.trim()
            : "";
        if (conversationId) {
            adapterContext.conversationId =
                conversationId;
        }
        propagateAdapterContextMetadataFields(adapterContext, metadata, [
            "clockDaemonId",
            "clockClientDaemonId",
            "clock_daemon_id",
            "clock_client_daemon_id",
            "tmuxSessionId",
            "tmux_session_id",
            "clientType",
            "clockClientType",
            "clientInjectReady",
            "clientInjectReason",
            "client_inject_ready",
            "client_inject_reason",
            "workdir",
            "cwd",
            "workingDirectory",
        ]);
        const clientConnectionState = metadata
            .clientConnectionState;
        if (clientConnectionState &&
            typeof clientConnectionState === "object" &&
            !Array.isArray(clientConnectionState)) {
            const stateRecord = clientConnectionState;
            adapterContext.clientConnectionState =
                clientConnectionState;
            if (typeof stateRecord.disconnected === "boolean") {
                adapterContext.clientDisconnected =
                    stateRecord.disconnected;
            }
        }
        const clientDisconnectedRaw = metadata
            .clientDisconnected;
        if (clientDisconnectedRaw === true ||
            (typeof clientDisconnectedRaw === "string" &&
                clientDisconnectedRaw.trim().toLowerCase() === "true")) {
            adapterContext.clientDisconnected = true;
        }
        if (target?.compatibilityProfile &&
            typeof target.compatibilityProfile === "string") {
            adapterContext.compatibilityProfile =
                target.compatibilityProfile;
        }
        return adapterContext;
    }
    applyMaxTokensPolicy(request, target) {
        if (!target) {
            return;
        }
        const params = request.parameters || (request.parameters = {});
        const direct = typeof params.max_tokens === "number" &&
            Number.isFinite(params.max_tokens)
            ? Math.floor(params.max_tokens)
            : undefined;
        const maxOutputRaw = typeof params.max_output_tokens ===
            "number" &&
            Number.isFinite(params.max_output_tokens)
            ? Math.floor(params.max_output_tokens)
            : undefined;
        let desired = direct ?? maxOutputRaw;
        // Default max tokens = 8k when client does not set.
        if (!desired || desired <= 0) {
            desired = 8192;
        }
        let providerCap = typeof target.maxOutputTokens === "number" &&
            Number.isFinite(target.maxOutputTokens)
            ? Math.floor(target.maxOutputTokens)
            : undefined;
        if (!providerCap) {
            const registry = this.routerEngine.providerRegistry;
            const profile = registry?.get?.(target.providerKey);
            const candidate = typeof profile?.maxOutputTokens === "number" &&
                Number.isFinite(profile.maxOutputTokens)
                ? Math.floor(profile.maxOutputTokens)
                : undefined;
            if (candidate && candidate > 0) {
                providerCap = candidate;
            }
        }
        if (providerCap && providerCap > 0) {
            desired = Math.min(desired, providerCap);
        }
        if (desired && desired > 0) {
            params.max_tokens = desired;
            if (params.max_output_tokens !== undefined) {
                params.max_output_tokens = desired;
            }
        }
    }
    maybeCreateStageRecorder(context, endpoint, options) {
        if (options?.disableSnapshots === true) {
            return undefined;
        }
        if (!shouldRecordSnapshots()) {
            return undefined;
        }
        const effectiveEndpoint = endpoint || context.entryEndpoint || "/v1/chat/completions";
        try {
            return createSnapshotRecorder(context, effectiveEndpoint);
        }
        catch {
            return undefined;
        }
    }
    asJsonObject(value) {
        if (!value || typeof value !== "object") {
            throw new Error("Responses pipeline requires JSON object payload");
        }
        return value;
    }
    async normalizeRequest(request) {
        if (!request || typeof request !== "object") {
            throw new Error("HubPipeline requires request payload");
        }
        const id = request.id || `req_${Date.now()}`;
        const endpoint = normalizeEndpoint(request.endpoint);
        const metadataRecord = {
            ...(request.metadata ?? {}),
        };
        const policyOverride = extractHubPolicyOverride(metadataRecord);
        if (Object.prototype.hasOwnProperty.call(metadataRecord, "__hubPolicyOverride")) {
            delete metadataRecord.__hubPolicyOverride;
        }
        const shadowCompare = extractHubShadowCompareConfig(metadataRecord);
        if (Object.prototype.hasOwnProperty.call(metadataRecord, "__hubShadowCompare")) {
            delete metadataRecord.__hubShadowCompare;
        }
        const disableSnapshots = metadataRecord.__disableHubSnapshots === true;
        if (Object.prototype.hasOwnProperty.call(metadataRecord, "__disableHubSnapshots")) {
            delete metadataRecord.__disableHubSnapshots;
        }
        const hubEntryRaw = typeof metadataRecord.__hubEntry === "string"
            ? String(metadataRecord.__hubEntry)
                .trim()
                .toLowerCase()
            : "";
        const hubEntryMode = hubEntryRaw === "chat_process" ||
            hubEntryRaw === "chat-process" ||
            hubEntryRaw === "chatprocess"
            ? "chat_process"
            : undefined;
        if (Object.prototype.hasOwnProperty.call(metadataRecord, "__hubEntry")) {
            delete metadataRecord.__hubEntry;
        }
        const entryEndpoint = typeof metadataRecord.entryEndpoint === "string"
            ? normalizeEndpoint(metadataRecord.entryEndpoint)
            : endpoint;
        const providerProtocol = resolveProviderProtocol(metadataRecord.providerProtocol);
        const processMode = metadataRecord.processMode === "passthrough" ? "passthrough" : "chat";
        const direction = metadataRecord.direction === "response" ? "response" : "request";
        const stage = metadataRecord.stage === "outbound" ? "outbound" : "inbound";
        const resolvedReadable = this.unwrapReadable(request.payload);
        const stream = Boolean(metadataRecord.stream ||
            resolvedReadable ||
            (request.payload &&
                typeof request.payload === "object" &&
                request.payload.stream));
        let payload = await this.materializePayload(request.payload, {
            requestId: id,
            entryEndpoint,
            providerProtocol,
            metadata: metadataRecord,
        }, resolvedReadable);
        const routeHint = typeof metadataRecord.routeHint === "string"
            ? metadataRecord.routeHint
            : undefined;
        const orchestrationResult = runHubPipelineOrchestrationWithNative({
            requestId: id,
            endpoint,
            entryEndpoint,
            providerProtocol,
            payload,
            metadata: {
                entryEndpoint,
                providerProtocol,
                processMode,
                direction,
                stage,
                stream,
                ...(routeHint ? { routeHint } : {}),
            },
            stream,
            processMode,
            direction,
            stage,
        });
        if (!orchestrationResult.success) {
            const code = orchestrationResult.error &&
                typeof orchestrationResult.error.code === "string"
                ? orchestrationResult.error.code.trim()
                : "hub_pipeline_native_failed";
            const message = orchestrationResult.error &&
                typeof orchestrationResult.error.message === "string"
                ? orchestrationResult.error.message.trim()
                : "Native hub pipeline orchestration failed";
            throw new Error(`[${code}] ${message}`);
        }
        if (orchestrationResult.payload) {
            payload = orchestrationResult.payload;
        }
        const normalizedMetadata = {
            ...metadataRecord,
            entryEndpoint,
            providerProtocol,
            processMode,
            direction,
            stage,
            stream,
            ...(routeHint ? { routeHint } : {}),
            ...(orchestrationResult.metadata ?? {}),
        };
        return {
            id,
            endpoint,
            entryEndpoint,
            providerProtocol,
            payload,
            metadata: normalizedMetadata,
            policyOverride: policyOverride ?? undefined,
            shadowCompare: shadowCompare ?? undefined,
            disableSnapshots,
            processMode,
            direction,
            stage,
            stream,
            routeHint,
            ...(hubEntryMode ? { hubEntryMode } : {}),
        };
    }
    convertProcessNodeResult(id, result) {
        return {
            id,
            success: result.success,
            metadata: result.metadata,
            error: result.error
                ? {
                    code: result.error.code ?? "hub_chat_process_error",
                    message: result.error.message,
                    details: result.error.details,
                }
                : undefined,
        };
    }
    async materializePayload(payload, context, resolvedStream) {
        const stream = resolvedStream ?? this.unwrapReadable(payload);
        if (stream) {
            return await this.convertSsePayload(stream, context);
        }
        if (!payload || typeof payload !== "object") {
            throw new Error("HubPipeline requires JSON object payload");
        }
        return payload;
    }
    unwrapReadable(payload) {
        if (!payload) {
            return null;
        }
        if (payload instanceof Readable) {
            return payload;
        }
        if (payload && typeof payload === "object" && "readable" in payload) {
            const candidate = payload.readable;
            if (candidate instanceof Readable) {
                return candidate;
            }
        }
        return null;
    }
    async convertSsePayload(stream, context) {
        const protocol = this.resolveSseProtocol(context);
        const codec = defaultSseCodecRegistry.get(protocol);
        try {
            const result = await codec.convertSseToJson(stream, {
                requestId: context.requestId,
                model: this.extractModelHint(context.metadata),
                direction: "request",
            });
            if (!result || typeof result !== "object") {
                throw new Error("SSE conversion returned empty payload");
            }
            return result;
        }
        catch (error) {
            const message = error instanceof Error
                ? error.message
                : String(error ?? "Unknown error");
            throw new Error(`Failed to convert SSE payload for protocol ${protocol}: ${message}`);
        }
    }
    resolveSseProtocol(context) {
        const explicitProtocol = resolveSseProtocolFromMetadata(context.metadata);
        if (explicitProtocol) {
            return explicitProtocol;
        }
        return context.providerProtocol;
    }
    extractModelHint(metadata) {
        if (typeof metadata.model === "string" && metadata.model.trim()) {
            return metadata.model;
        }
        const provider = metadata.provider;
        const candidates = [
            provider?.model,
            provider?.modelId,
            provider?.defaultModel,
        ];
        for (const candidate of candidates) {
            if (typeof candidate === "string" && candidate.trim()) {
                return candidate;
            }
        }
        return undefined;
    }
    resolveOutboundStreamIntent(providerPreference) {
        return resolveOutboundStreamIntentWithNative(providerPreference);
    }
    applyOutboundStreamPreference(request, stream, processMode) {
        if (!request || typeof request !== "object") {
            return request;
        }
        return applyOutboundStreamPreferenceWithNative(request, stream, processMode);
    }
}
function normalizeEndpoint(endpoint) {
    return normalizeHubEndpointWithNative(endpoint);
}
function resolveProviderProtocol(value) {
    try {
        const normalized = resolveHubProviderProtocolWithNative(value);
        if (normalized === "openai-chat" ||
            normalized === "openai-responses" ||
            normalized === "anthropic-messages" ||
            normalized === "gemini-chat") {
            return normalized;
        }
    }
    catch {
        // Keep legacy caller-facing error shape below.
    }
    throw new Error(`[HubPipeline] Unsupported providerProtocol "${value}". Configure a valid protocol (openai-chat|openai-responses|anthropic-messages|gemini-chat).`);
}
function resolveSseProtocolFromMetadata(metadata) {
    const resolved = resolveHubSseProtocolFromMetadataWithNative(metadata);
    if (!resolved) {
        return undefined;
    }
    return resolveProviderProtocol(resolved);
}
function coerceAliasMap(candidate) {
    return normalizeAliasMapWithNative(candidate);
}
function readAliasMapFromSemantics(chatEnvelope) {
    if (!chatEnvelope?.semantics ||
        typeof chatEnvelope.semantics !== "object" ||
        Array.isArray(chatEnvelope.semantics)) {
        return undefined;
    }
    return resolveAliasMapFromRespSemanticsWithNative(chatEnvelope.semantics);
}
function assertNoMappableSemanticsInMetadata(metadata, scope) {
    if (!metadata || typeof metadata !== "object") {
        return;
    }
    const present = findMappableSemanticsKeysWithNative(metadata);
    if (present.length) {
        throw new Error(`[HubPipeline][semantic_gate] Mappable semantics must not be stored in metadata (${scope}): ${present.join(", ")}`);
    }
}
