"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.handleProxyModelsRequest = exports.handleProxyMessagesRequest = exports.attachDisconnectAbortHandlers = exports.validateIncomingProxyAuth = exports.extractIncomingProxyToken = void 0;
const undici_1 = require("undici");
const request_router_1 = require("../request-router");
const request_transformer_1 = require("../transformers/request-transformer");
const sse_stream_transformer_1 = require("../transformers/sse-stream-transformer");
const upstream_url_1 = require("../upstream-url");
const logging_1 = require("../../services/logging");
const http_helpers_1 = require("./http-helpers");
const REQUEST_TIMEOUT_MS = 600000;
const DIRECT_OPENAI_REASONING_CHAT_MODEL = /^(?:gpt-5|o[134])(?:[-.]|$)/;
const logger = (0, logging_1.createLogger)('proxy:openai-compat:messages');
class ProxyInputError extends Error {
    constructor(message) {
        super(message);
        this.name = 'ProxyInputError';
    }
}
function buildUpstreamHeaders(profile) {
    return {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${profile.apiKey}`,
        'User-Agent': 'CCS-OpenAI-Compat-Proxy/1.0',
    };
}
function isDirectOpenAIReasoningChatModel(profile, model) {
    return (profile.provider === 'openai' &&
        typeof model === 'string' &&
        DIRECT_OPENAI_REASONING_CHAT_MODEL.test(model.trim().toLowerCase()));
}
function shapeUpstreamChatPayload(payload, profile) {
    if (!isDirectOpenAIReasoningChatModel(profile, payload.model)) {
        return payload;
    }
    const shaped = { ...payload };
    if (shaped.max_tokens !== undefined) {
        shaped.max_completion_tokens = shaped.max_tokens;
        delete shaped.max_tokens;
    }
    delete shaped.metadata;
    if ((shaped.tools?.length ?? 0) > 0) {
        delete shaped.reasoning_effort;
    }
    return shaped;
}
function buildUpstreamRequest(profile, rawBody) {
    let transformed;
    try {
        const transformer = new request_transformer_1.ProxyRequestTransformer();
        transformed = transformer.transform(rawBody);
    }
    catch (error) {
        const message = error instanceof Error ? error.message : 'Invalid Anthropic request';
        throw new ProxyInputError(message);
    }
    const route = (0, request_router_1.resolveProxyRequestRoute)(profile, transformed);
    const body = shapeUpstreamChatPayload({
        ...transformed,
        model: route.model || route.profile.model,
        stream: transformed.stream === true,
    }, route.profile);
    return { body: JSON.stringify(body), route };
}
function extractIncomingProxyToken(headers) {
    const xApiKey = headers['x-api-key'];
    if (typeof xApiKey === 'string' && xApiKey.trim().length > 0) {
        return xApiKey.trim();
    }
    const anthropicApiKey = headers['anthropic-api-key'];
    if (typeof anthropicApiKey === 'string' && anthropicApiKey.trim().length > 0) {
        return anthropicApiKey.trim();
    }
    const authHeader = headers.authorization;
    if (typeof authHeader === 'string' && authHeader.trim().length > 0) {
        const trimmed = authHeader.trim();
        const bearerPrefix = 'Bearer ';
        return trimmed.startsWith(bearerPrefix) ? trimmed.slice(bearerPrefix.length).trim() : trimmed;
    }
    return null;
}
exports.extractIncomingProxyToken = extractIncomingProxyToken;
function validateIncomingProxyAuth(headers, expectedToken) {
    return extractIncomingProxyToken(headers) === expectedToken;
}
exports.validateIncomingProxyAuth = validateIncomingProxyAuth;
function buildFetchInit(profile, body, signal, insecureDispatcher) {
    const init = {
        method: 'POST',
        headers: buildUpstreamHeaders(profile),
        body,
        signal,
    };
    if (insecureDispatcher) {
        init.dispatcher = insecureDispatcher;
    }
    return init;
}
function getRequestTimeoutMs() {
    const rawValue = process.env.CCS_OPENAI_PROXY_REQUEST_TIMEOUT_MS;
    if (!rawValue) {
        return REQUEST_TIMEOUT_MS;
    }
    const parsed = Number.parseInt(rawValue, 10);
    return Number.isFinite(parsed) && parsed > 0 ? parsed : REQUEST_TIMEOUT_MS;
}
function formatTimeoutDuration(timeoutMs) {
    return timeoutMs % 1000 === 0 ? `${timeoutMs / 1000} seconds` : `${timeoutMs}ms`;
}
function registerOnceListener(emitter, event, handler) {
    if (!emitter) {
        return () => { };
    }
    emitter.once(event, handler);
    return () => {
        emitter.removeListener(event, handler);
    };
}
function attachDisconnectAbortHandlers(req, res, controller, onDisconnect) {
    const abortOnDisconnect = (source) => {
        if (!controller.signal.aborted && !res.writableEnded) {
            onDisconnect(source);
            controller.abort();
        }
    };
    const cleanupFns = [
        registerOnceListener(req, 'aborted', () => abortOnDisconnect('req.aborted')),
        registerOnceListener(req.socket, 'close', () => abortOnDisconnect('req.socket.close')),
        registerOnceListener(res.socket, 'close', () => abortOnDisconnect('res.socket.close')),
    ];
    const disconnectPoll = setInterval(() => {
        if (req.socket?.destroyed === true) {
            abortOnDisconnect('poll.socket.destroyed');
        }
    }, 50);
    return () => {
        clearInterval(disconnectPoll);
        for (const cleanup of cleanupFns) {
            cleanup();
        }
    };
}
exports.attachDisconnectAbortHandlers = attachDisconnectAbortHandlers;
async function handleProxyMessagesRequest(req, res, profile, expectedAuthToken, insecureDispatcher) {
    const transformer = new sse_stream_transformer_1.ProxySseStreamTransformer();
    const startedAt = Date.now();
    logger.stage('intake', 'request.received', 'Proxy /v1/messages request received', {
        method: req.method || 'POST',
        remoteAddress: req.socket.remoteAddress || null,
    });
    if (!validateIncomingProxyAuth(req.headers, expectedAuthToken)) {
        logger.stage('auth', 'auth.invalid', 'Rejected proxy message request with invalid auth token', {
            remoteAddress: req.socket.remoteAddress || null,
        }, { level: 'warn' });
        await (0, http_helpers_1.pipeWebResponseToNode)(transformer.error(401, 'authentication_error', 'Missing or invalid local proxy token'), res);
        return;
    }
    logger.stage('auth', 'auth.ok', 'Proxy auth validated');
    let timeoutMs = REQUEST_TIMEOUT_MS;
    try {
        const rawBody = await (0, http_helpers_1.readJsonBody)(req);
        logger.stage('transform', 'request.transform.start', 'Transforming inbound proxy body');
        const upstream = buildUpstreamRequest(profile, rawBody);
        logger.stage('route', 'request.routed', 'Resolved proxy upstream route', {
            profileName: upstream.route.profile.profileName,
            provider: upstream.route.profile.provider,
            baseUrl: upstream.route.profile.baseUrl,
            model: upstream.route.model || upstream.route.profile.model || null,
            routeSource: upstream.route.source,
            scenario: upstream.route.scenario || null,
            estimatedTokens: upstream.route.estimatedTokens,
        });
        const controller = new AbortController();
        timeoutMs = getRequestTimeoutMs();
        const timeout = setTimeout(() => controller.abort(), timeoutMs);
        const cleanupDisconnectHandlers = attachDisconnectAbortHandlers(req, res, controller, (source) => {
            logger.stage('cleanup', 'request.disconnect', 'Aborting upstream request after local client disconnect', {
                profileName: profile.profileName,
                source,
            });
        });
        const useSharedInsecureDispatcher = insecureDispatcher !== undefined &&
            upstream.route.profile.profileName === profile.profileName;
        const useProfileInsecureTls = upstream.route.profile.insecure === true;
        const ephemeralInsecureDispatcher = useProfileInsecureTls && !useSharedInsecureDispatcher
            ? new undici_1.Agent({ connect: { rejectUnauthorized: false } })
            : undefined;
        const dispatcher = useSharedInsecureDispatcher
            ? insecureDispatcher
            : useProfileInsecureTls
                ? ephemeralInsecureDispatcher
                : undefined;
        try {
            logger.stage('dispatch', 'upstream.dispatch', 'Dispatching upstream fetch', {
                profileName: profile.profileName,
                routedProfileName: upstream.route.profile.profileName,
                insecureTls: dispatcher !== undefined,
            });
            const upstreamResponse = await fetch((0, upstream_url_1.resolveOpenAIChatCompletionsUrl)(upstream.route.profile.baseUrl), buildFetchInit(upstream.route.profile, upstream.body, controller.signal, dispatcher));
            logger.stage('upstream', 'upstream.response', 'Received upstream response', {
                profileName: profile.profileName,
                routedProfileName: upstream.route.profile.profileName,
                status: upstreamResponse.status,
            });
            const response = await transformer.transform(upstreamResponse);
            await (0, http_helpers_1.pipeWebResponseToNode)(response, res);
            logger.stage('respond', 'request.respond', 'Proxy response written', undefined, {
                latencyMs: Date.now() - startedAt,
            });
        }
        finally {
            clearTimeout(timeout);
            cleanupDisconnectHandlers();
            if (ephemeralInsecureDispatcher) {
                try {
                    await ephemeralInsecureDispatcher.close();
                }
                catch (closeError) {
                    logger.stage('cleanup', 'request.dispatcher_close_failed', 'Failed to close per-request insecure dispatcher', {
                        profileName: profile.profileName,
                        routedProfileName: upstream.route.profile.profileName,
                        error: closeError instanceof Error ? closeError.message : String(closeError),
                    }, { level: 'warn' });
                }
            }
        }
    }
    catch (error) {
        const message = error instanceof Error ? error.message : 'Unknown proxy error';
        const errInfo = {
            name: error instanceof Error ? error.name : 'Error',
            message,
        };
        logger.stage('cleanup', 'request.failed', 'Proxy message request failed', {
            profileName: profile.profileName,
            abort: error instanceof Error && error.name === 'AbortError',
        }, {
            level: 'error',
            latencyMs: Date.now() - startedAt,
            error: errInfo,
        });
        const status = error instanceof Error && error.name === 'AbortError'
            ? 502
            : error instanceof ProxyInputError
                ? 400
                : message.includes('Request body too large')
                    ? 413
                    : message.includes('Invalid JSON')
                        ? 400
                        : 502;
        const type = status >= 500 ? 'api_error' : 'invalid_request_error';
        await (0, http_helpers_1.pipeWebResponseToNode)(transformer.error(status, type, error instanceof Error && error.name === 'AbortError'
            ? `The upstream provider did not respond within ${formatTimeoutDuration(timeoutMs)}`
            : message), res);
    }
}
exports.handleProxyMessagesRequest = handleProxyMessagesRequest;
function handleProxyModelsRequest(res, profile) {
    const data = [profile.model, profile.opusModel, profile.sonnetModel, profile.haikuModel]
        .filter((value) => typeof value === 'string' && value.length > 0)
        .map((id) => ({
        id,
        object: 'model',
        created: 0,
        owned_by: profile.provider,
    }));
    (0, http_helpers_1.writeJson)(res, 200, { object: 'list', data });
}
exports.handleProxyModelsRequest = handleProxyModelsRequest;
//# sourceMappingURL=messages-route.js.map