import { MIDSCENE_LANGFUSE_DEBUG, MIDSCENE_LANGSMITH_DEBUG, globalConfigManager } from "@midscene/shared/env";
import { getDebug } from "@midscene/shared/logger";
import { assert, ifInBrowser } from "@midscene/shared/utils";
import openai_0 from "openai";
import { getModelRuntime } from "../models/index.mjs";
import { callAIWithCodexAppServer, isCodexAppServerProvider } from "./codex-app-server.mjs";
import { buildRequestAbortSignal, isHardTimeoutError, resolveEffectiveTimeoutMs } from "./request-timeout.mjs";
import { extractJSONFromCodeBlock, normalJsonParser, safeParseJson } from "./json.mjs";
function _define_property(obj, key, value) {
    if (key in obj) Object.defineProperty(obj, key, {
        value: value,
        enumerable: true,
        configurable: true,
        writable: true
    });
    else obj[key] = value;
    return obj;
}
class AIResponseParseError extends Error {
    constructor(message, rawResponse, usage, rawChoiceMessage){
        super(message), _define_property(this, "usage", void 0), _define_property(this, "rawResponse", void 0), _define_property(this, "rawChoiceMessage", void 0);
        this.name = 'AIResponseParseError';
        this.rawResponse = rawResponse;
        this.usage = usage;
        this.rawChoiceMessage = rawChoiceMessage;
    }
}
function stringifyForDebug(value) {
    try {
        return JSON.stringify(value);
    } catch (_error) {
        return String(value);
    }
}
async function createChatClient({ modelConfig }) {
    const { socksProxy, httpProxy, modelName, openaiBaseURL, openaiApiKey, openaiExtraConfig, modelDescription, modelFamily, createOpenAIClient, timeout } = modelConfig;
    let proxyAgent;
    const warnClient = getDebug('ai:call', {
        console: true
    });
    const debugProxy = getDebug('ai:call:proxy');
    const warnProxy = getDebug('ai:call:proxy', {
        console: true
    });
    const sanitizeProxyUrl = (url)=>{
        try {
            const parsed = new URL(url);
            if (parsed.username) {
                parsed.password = '****';
                return parsed.href;
            }
            return url;
        } catch  {
            return url;
        }
    };
    if (httpProxy) {
        debugProxy('using http proxy', sanitizeProxyUrl(httpProxy));
        if (ifInBrowser) warnProxy('HTTP proxy is configured but not supported in browser environment');
        else {
            const moduleName = 'undici';
            const { ProxyAgent } = await import(moduleName);
            proxyAgent = new ProxyAgent({
                uri: httpProxy
            });
        }
    } else if (socksProxy) {
        debugProxy('using socks proxy', sanitizeProxyUrl(socksProxy));
        if (ifInBrowser) warnProxy('SOCKS proxy is configured but not supported in browser environment');
        else try {
            const moduleName = 'fetch-socks';
            const { socksDispatcher } = await import(moduleName);
            const proxyUrl = new URL(socksProxy);
            if (!proxyUrl.hostname) throw new Error('SOCKS proxy URL must include a valid hostname');
            const port = Number.parseInt(proxyUrl.port, 10);
            if (!proxyUrl.port || Number.isNaN(port)) throw new Error('SOCKS proxy URL must include a valid port');
            const protocol = proxyUrl.protocol.replace(':', '');
            const socksType = 'socks4' === protocol ? 4 : 'socks5' === protocol ? 5 : 5;
            proxyAgent = socksDispatcher({
                type: socksType,
                host: proxyUrl.hostname,
                port,
                ...proxyUrl.username ? {
                    userId: decodeURIComponent(proxyUrl.username),
                    password: decodeURIComponent(proxyUrl.password || '')
                } : {}
            });
            debugProxy('socks proxy configured successfully', {
                type: socksType,
                host: proxyUrl.hostname,
                port: port
            });
        } catch (error) {
            warnProxy('Failed to configure SOCKS proxy:', error);
            throw new Error(`Invalid SOCKS proxy URL: ${socksProxy}. Expected format: socks4://host:port, socks5://host:port, or with authentication: socks5://user:pass@host:port`);
        }
    }
    const effectiveTimeoutMs = resolveEffectiveTimeoutMs({
        timeout
    });
    const openAIOptions = {
        baseURL: openaiBaseURL,
        apiKey: openaiApiKey,
        ...proxyAgent ? {
            fetchOptions: {
                dispatcher: proxyAgent
            }
        } : {},
        ...openaiExtraConfig,
        maxRetries: 0,
        ...null !== effectiveTimeoutMs ? {
            timeout: effectiveTimeoutMs
        } : {},
        dangerouslyAllowBrowser: true
    };
    const baseOpenAI = new openai_0(openAIOptions);
    let openai = baseOpenAI;
    if (openai && globalConfigManager.getEnvConfigInBoolean(MIDSCENE_LANGSMITH_DEBUG)) {
        if (ifInBrowser) throw new Error('langsmith is not supported in browser');
        warnClient('DEBUGGING MODE: langsmith wrapper enabled');
        const langsmithModule = 'langsmith/wrappers';
        const { wrapOpenAI } = await import(langsmithModule);
        openai = wrapOpenAI(openai);
    }
    if (openai && globalConfigManager.getEnvConfigInBoolean(MIDSCENE_LANGFUSE_DEBUG)) {
        if (ifInBrowser) throw new Error('langfuse is not supported in browser');
        warnClient('DEBUGGING MODE: langfuse wrapper enabled');
        const langfuseModule = '@langfuse/openai';
        const { observeOpenAI } = await import(langfuseModule);
        openai = observeOpenAI(openai);
    }
    if (createOpenAIClient) {
        const wrappedClient = await createOpenAIClient(baseOpenAI, openAIOptions);
        if (wrappedClient) openai = wrappedClient;
    }
    return {
        completion: openai.chat.completions,
        modelName,
        modelDescription,
        modelFamily
    };
}
async function callAI(messages, modelRuntime, options) {
    const { config: modelConfig, adapter } = modelRuntime;
    if (isCodexAppServerProvider(modelConfig.openaiBaseURL)) return callAIWithCodexAppServer(messages, modelConfig, {
        stream: options?.stream,
        onChunk: options?.onChunk,
        reasoningEnabled: modelConfig.reasoningEnabled,
        abortSignal: options?.abortSignal
    });
    const { completion, modelName, modelDescription, modelFamily } = await createChatClient({
        modelConfig
    });
    const effectiveTimeoutMs = resolveEffectiveTimeoutMs(modelConfig);
    const extraBody = modelConfig.extraBody;
    const debugCall = getDebug('ai:call');
    const warnCall = getDebug('ai:call', {
        console: true
    });
    const debugProfileStats = getDebug('ai:profile:stats');
    const debugProfileDetail = getDebug('ai:profile:detail');
    const startTime = Date.now();
    const isStreaming = options?.stream && options?.onChunk;
    const chatCompletionInput = {
        intent: modelConfig.intent,
        userConfig: {
            temperature: modelConfig.temperature,
            reasoningEnabled: modelConfig.reasoningEnabled,
            reasoningEffort: modelConfig.reasoningEffort,
            reasoningBudget: modelConfig.reasoningBudget
        },
        requiresOriginalImageDetail: options?.requiresOriginalImageDetail
    };
    const { config: adapterChatCompletionParams } = adapter.chatCompletion.buildChatCompletionParams(chatCompletionInput);
    debugCall(`adapter chat completion params: ${stringifyForDebug({
        config: adapterChatCompletionParams
    })}`);
    let content;
    let accumulated = '';
    let accumulatedReasoning = '';
    let rawChoiceMessage;
    let usage;
    let timeCost;
    let requestId;
    let responseModelName;
    const hasUsableText = (value)=>'string' == typeof value && value.trim().length > 0;
    const buildUsageInfo = (usageData, requestId)=>{
        if (!usageData) return;
        const cachedInputTokens = usageData?.prompt_tokens_details?.cached_tokens;
        return {
            ...usageData,
            prompt_tokens: usageData.prompt_tokens ?? 0,
            completion_tokens: usageData.completion_tokens ?? 0,
            total_tokens: usageData.total_tokens ?? 0,
            cached_input: cachedInputTokens ?? 0,
            time_cost: timeCost ?? 0,
            model_name: modelName,
            model_description: modelDescription,
            response_model_name: responseModelName,
            slot: modelConfig.slot,
            intent: void 0,
            request_id: requestId ?? void 0
        };
    };
    const requestConfig = {
        ...adapterChatCompletionParams,
        ...extraBody ?? {}
    };
    const temperature = requestConfig.temperature;
    const imageDetail = adapter.chatCompletion.resolveImageDetail(chatCompletionInput);
    const messagesWithImageDetail = (()=>{
        if (!imageDetail) return messages;
        return messages.map((msg)=>{
            if (!Array.isArray(msg.content)) return msg;
            const content = msg.content.map((part)=>{
                if (part && 'image_url' === part.type && part.image_url?.url) return {
                    ...part,
                    image_url: {
                        ...part.image_url,
                        detail: imageDetail
                    }
                };
                return part;
            });
            return {
                ...msg,
                content
            };
        });
    })();
    try {
        debugCall(`sending ${isStreaming ? 'streaming ' : ''}request to ${modelName}`);
        if (isStreaming) {
            const { signal: streamSignal, cleanup: cleanupStreamSignal } = buildRequestAbortSignal(effectiveTimeoutMs, options?.abortSignal);
            try {
                const stream = await completion.create({
                    model: modelName,
                    messages: messagesWithImageDetail,
                    ...requestConfig,
                    stream: true
                }, {
                    stream: true,
                    signal: streamSignal
                });
                requestId = stream._request_id;
                for await (const chunk of stream){
                    const parsedChunk = adapter.chatCompletion.extractContentAndReasoning(chunk.choices?.[0]?.delta);
                    const content = parsedChunk.content || '';
                    const reasoning_content = parsedChunk.reasoning_content || '';
                    if (chunk.usage) usage = chunk.usage;
                    if (chunk.model) responseModelName = chunk.model;
                    if (content || reasoning_content) {
                        accumulated += content;
                        accumulatedReasoning += reasoning_content;
                        const chunkData = {
                            content,
                            reasoning_content,
                            accumulated,
                            isComplete: false,
                            usage: void 0
                        };
                        options.onChunk(chunkData);
                    }
                    if (chunk.choices?.[0]?.finish_reason) {
                        timeCost = Date.now() - startTime;
                        if (!usage) {
                            const estimatedTokens = Math.max(1, Math.floor(accumulated.length / 4));
                            usage = {
                                prompt_tokens: estimatedTokens,
                                completion_tokens: estimatedTokens,
                                total_tokens: 2 * estimatedTokens
                            };
                        }
                        const finalChunk = {
                            content: '',
                            accumulated,
                            reasoning_content: '',
                            isComplete: true,
                            usage: buildUsageInfo(usage, requestId)
                        };
                        options.onChunk(finalChunk);
                        break;
                    }
                }
            } finally{
                cleanupStreamSignal();
            }
            content = accumulated;
            debugProfileStats(`streaming model, ${modelName}, mode, ${modelFamily || 'default'}, cost-ms, ${timeCost}, temperature, ${temperature ?? ''}`);
        } else {
            const retryCount = modelConfig.retryCount ?? 1;
            const retryInterval = modelConfig.retryInterval ?? 2000;
            const maxAttempts = retryCount + 1;
            let lastError;
            for(let attempt = 1; attempt <= maxAttempts; attempt++){
                const { signal: attemptSignal, cleanup: cleanupAttemptSignal } = buildRequestAbortSignal(effectiveTimeoutMs, options?.abortSignal);
                try {
                    const result = await completion.create({
                        model: modelName,
                        messages: messagesWithImageDetail,
                        ...requestConfig,
                        stream: false
                    }, {
                        signal: attemptSignal
                    });
                    timeCost = Date.now() - startTime;
                    debugProfileStats(`model, ${modelName}, mode, ${modelFamily || 'default'}, prompt-tokens, ${result.usage?.prompt_tokens || ''}, completion-tokens, ${result.usage?.completion_tokens || ''}, total-tokens, ${result.usage?.total_tokens || ''}, cost-ms, ${timeCost}, requestId, ${result._request_id || ''}, temperature, ${temperature ?? ''}`);
                    debugProfileDetail(`model usage detail: ${JSON.stringify(result.usage)}`);
                    if (!result.choices) throw new Error(`invalid response from LLM service: ${JSON.stringify(result)}`);
                    rawChoiceMessage = result.choices[0].message;
                    const parsedMessage = adapter.chatCompletion.extractContentAndReasoning(result.choices[0].message);
                    content = parsedMessage.content;
                    accumulatedReasoning = parsedMessage.reasoning_content;
                    usage = result.usage;
                    requestId = result._request_id;
                    responseModelName = result.model;
                    if (!hasUsableText(content) && hasUsableText(accumulatedReasoning)) {
                        warnCall('empty content from AI model, using reasoning content');
                        content = accumulatedReasoning;
                    }
                    if (!hasUsableText(content)) throw new AIResponseParseError('empty content from AI model', JSON.stringify(result), buildUsageInfo(usage, requestId), rawChoiceMessage);
                    break;
                } catch (error) {
                    lastError = error;
                    const wasHardTimeout = isHardTimeoutError(lastError);
                    if (wasHardTimeout) warnCall(`AI call hit hard timeout (${effectiveTimeoutMs}ms, attempt ${attempt}/${maxAttempts}, model ${modelName}, slot ${modelConfig.slot})`);
                    if (options?.abortSignal?.aborted) break;
                    if (attempt < maxAttempts) {
                        warnCall(`AI call failed (attempt ${attempt}/${maxAttempts}), retrying in ${retryInterval}ms... Error: ${lastError.message}`);
                        await new Promise((resolve)=>setTimeout(resolve, retryInterval));
                    }
                } finally{
                    cleanupAttemptSignal();
                }
            }
            if (!content) throw lastError;
        }
        debugCall(`response reasoning content: ${accumulatedReasoning}`);
        debugCall(`response content: ${content}`);
        if (isStreaming && !usage) {
            const estimatedTokens = Math.max(1, Math.floor((content || '').length / 4));
            usage = {
                prompt_tokens: estimatedTokens,
                completion_tokens: estimatedTokens,
                total_tokens: 2 * estimatedTokens
            };
        }
        return {
            content: content || '',
            reasoning_content: accumulatedReasoning || void 0,
            rawChoiceMessage,
            usage: buildUsageInfo(usage, requestId),
            isStreamed: !!isStreaming
        };
    } catch (e) {
        warnCall('call AI error', e);
        if (e instanceof AIResponseParseError) throw e;
        const newError = new Error(`failed to call ${isStreaming ? 'streaming ' : ''}AI model service (${modelName}): ${e.message}\nTrouble shooting: https://midscenejs.com/model-provider.html`, {
            cause: e
        });
        throw newError;
    }
}
async function callAIWithObjectResponse(messages, model, options) {
    const modelRuntime = resolveCompatibleModelRuntime(model);
    const { config: modelConfig, adapter } = modelRuntime;
    const response = await callAI(messages, modelRuntime, {
        abortSignal: options?.abortSignal
    });
    assert(response, 'empty response');
    const jsonContent = adapter.jsonParser(response.content, {
        source: options?.jsonParserSource ?? 'generic-object'
    });
    if ('object' != typeof jsonContent) throw new AIResponseParseError(`failed to parse json response from model (${modelConfig.modelName}): ${response.content}`, response.content, response.usage, response.rawChoiceMessage);
    return {
        content: jsonContent,
        contentString: response.content,
        usage: response.usage,
        reasoning_content: response.reasoning_content,
        rawChoiceMessage: response.rawChoiceMessage
    };
}
function resolveCompatibleModelRuntime(model) {
    if ('config' in model && 'adapter' in model) return model;
    return getModelRuntime(model);
}
async function callAIWithStringResponse(msgs, modelRuntime, options) {
    const { content, usage, rawChoiceMessage } = await callAI(msgs, modelRuntime, {
        abortSignal: options?.abortSignal
    });
    return {
        content,
        usage,
        rawChoiceMessage
    };
}
export { AIResponseParseError, callAI, callAIWithObjectResponse, callAIWithStringResponse, createChatClient, extractJSONFromCodeBlock, normalJsonParser, safeParseJson };

//# sourceMappingURL=index.mjs.map