"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GenericOpenAiClient = void 0;
const openai_1 = __importDefault(require("openai"));
const config_1 = require("../config");
const pricing_1 = require("./pricing");
const contextLimits_1 = require("./contextLimits");
const types_1 = require("../types");
const config = (0, config_1.getConfigSync)();
class GenericOpenAiClient {
    client;
    apiKey;
    constructor(apiKey = process.env.OPENAI_KEY) {
        this.setKey(apiKey);
    }
    setKey(apiKey) {
        this.apiKey = apiKey;
        this.client = new openai_1.default({
            apiKey,
            ...(config?.openaiBaseUrl && { baseURL: config.openaiBaseUrl }),
        });
    }
    reasoningEffort(messages) {
        return this.detectReasoningEffort(messages);
    }
    detectReasoningEffort(messages) {
        const effortMap = {
            ultrathink: "high",
            "think hard": "high",
            "reason hard": "high",
            "think carefully": "medium",
            "reason carefully": "medium",
            "think medium": "medium",
            "reason medium": "medium",
            "think low": "low",
            "reason low": "low",
            "think simple": "low",
            "reason simple": "low",
        };
        for (const key in effortMap) {
            if (messages.some((msg) => typeof msg.content === "string" &&
                msg.role === "user" &&
                msg.content?.includes(key))) {
                return effortMap[key];
            }
        }
        return "medium";
    }
    resolveReasoningEffort(options) {
        return options.reasoning_effort ?? this.detectReasoningEffort(options.messages);
    }
    resolveReasoningEffortForModel(options) {
        const requested = options.reasoning_effort ?? this.detectReasoningEffort(options.messages);
        const pricing = pricing_1.OpenAiTextPricing[options.model];
        const supportedLevels = pricing?.reasoningLevels;
        if (!supportedLevels || supportedLevels.length === 0) {
            return requested;
        }
        if (supportedLevels.includes(requested)) {
            return requested;
        }
        return supportedLevels[0];
    }
    async createChatCompletion(options) {
        if (types_1.OpenAiResponsesOnlyModels.includes(options.model)) {
            return this.createChatResponse(options);
        }
        const openaiMessages = options.messages.map((msg) => {
            if (msg.role === "tool") {
                return {
                    ...msg,
                    content: msg.content || "",
                    role: "tool",
                    tool_call_id: msg.tool_call_id,
                };
            }
            return msg;
        });
        const response = await this.client.chat.completions.create({
            model: options.model,
            messages: openaiMessages,
            max_tokens: options.max_tokens,
            ...(types_1.OpenAiReasoningModels.includes(options.model) && {
                max_tokens: undefined,
                max_completion_tokens: Math.max(options.max_tokens ?? 0, 16_000),
                reasoning_effort: this.resolveReasoningEffort(options),
            }),
            ...(options.tools && {
                tools: options.tools,
                tool_choice: "auto",
            }),
        }, { signal: options.signal });
        const usdCost = this.calculateCost(options.model, response.usage);
        return {
            choices: response.choices.map((choice) => ({
                message: {
                    role: choice.message?.role || "assistant",
                    content: choice.message?.content || null,
                    tool_calls: choice.message?.tool_calls
                        ? choice.message.tool_calls
                        : undefined,
                },
            })),
            model: options.model,
            usage: response.usage ? {
                prompt_tokens: response.usage.prompt_tokens ?? 0,
                completion_tokens: response.usage.completion_tokens ?? 0,
                total_tokens: response.usage.total_tokens,
                prompt_tokens_details: {
                    cached_tokens: response.usage.prompt_tokens_details?.cached_tokens ?? 0,
                },
            } : undefined,
            usd_cost: usdCost,
        };
    }
    repairTruncatedJson(args) {
        try {
            JSON.parse(args);
            return args;
        }
        catch {
            let repaired = args.trimEnd();
            let depth = 0;
            let inString = false;
            let escaped = false;
            for (const ch of repaired) {
                if (escaped) {
                    escaped = false;
                    continue;
                }
                if (ch === '\\' && inString) {
                    escaped = true;
                    continue;
                }
                if (ch === '"') {
                    inString = !inString;
                    continue;
                }
                if (!inString) {
                    if (ch === '{' || ch === '[')
                        depth++;
                    else if (ch === '}' || ch === ']')
                        depth--;
                }
            }
            if (inString)
                repaired += '"';
            for (let i = 0; i < depth; i++)
                repaired += '}';
            try {
                JSON.parse(repaired);
                return repaired;
            }
            catch {
                return args;
            }
        }
    }
    async createChatResponse(options) {
        const systemMessages = options.messages.filter((m) => m.role === "system");
        const nonSystemMessages = options.messages.filter((m) => m.role !== "system");
        const instructions = systemMessages
            .map((m) => (typeof m.content === "string" ? m.content : ""))
            .join("\n")
            .trim() || undefined;
        const input = nonSystemMessages.map((msg) => {
            if (msg.role === "tool") {
                return {
                    type: "function_call_output",
                    call_id: msg.tool_call_id,
                    output: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
                };
            }
            if (msg.role === "assistant" && msg.tool_calls?.length) {
                return msg.tool_calls.map((tc) => ({
                    type: "function_call",
                    id: tc.id.startsWith("fc") ? tc.id : `fc_${tc.id}`,
                    call_id: tc.id,
                    name: tc.function.name,
                    arguments: tc.function.arguments,
                }));
            }
            return {
                role: msg.role,
                content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
            };
        }).flat();
        const tools = options.tools?.map((tool) => ({
            type: "function",
            name: tool.function.name,
            description: tool.function.description,
            parameters: tool.function.parameters,
            strict: false,
        }));
        const response = await this.client.responses.create({
            model: options.model,
            input,
            ...(instructions && { instructions }),
            ...(types_1.OpenAiReasoningModels.includes(options.model) && {
                max_output_tokens: Math.max(options.max_tokens || 0, 16000),
                reasoning: { effort: this.resolveReasoningEffortForModel(options) },
            }),
            ...(tools?.length && {
                tools,
                tool_choice: "auto",
            }),
            store: false,
        });
        const usage = response.usage
            ? {
                prompt_tokens: response.usage.input_tokens,
                completion_tokens: response.usage.output_tokens,
                total_tokens: response.usage.input_tokens + response.usage.output_tokens,
                prompt_tokens_details: {
                    cached_tokens: response.usage.input_tokens_details?.cached_tokens ?? 0,
                },
            }
            : undefined;
        const usdCost = usage
            ? this.calculateCost(options.model, usage)
            : undefined;
        let textContent = null;
        const toolCalls = [];
        for (const item of response.output) {
            if (item.type === "message") {
                const msgItem = item;
                for (const part of msgItem.content ?? []) {
                    if (part.type === "output_text") {
                        textContent = (textContent ?? "") + part.text;
                    }
                }
            }
            else if (item.type === "function_call") {
                const fc = item;
                const repairedArgs = this.repairTruncatedJson(fc.arguments);
                try {
                    JSON.parse(repairedArgs);
                }
                catch (e) {
                    console.warn(`[Responses API] Invalid JSON arguments for ${fc.name} after repair: ${e.message}`);
                }
                toolCalls.push({
                    id: fc.call_id,
                    type: "function",
                    function: {
                        name: fc.name,
                        arguments: repairedArgs,
                    },
                });
            }
        }
        return {
            choices: [
                {
                    message: {
                        role: "assistant",
                        content: textContent,
                        ...(toolCalls.length > 0 && { tool_calls: toolCalls }),
                    },
                },
            ],
            model: options.model,
            usage,
            usd_cost: usdCost,
        };
    }
    pricesPerMillion() {
        return pricing_1.OpenAiTextPricing;
    }
    calculateCost(model, usage) {
        const pricing = this.pricesPerMillion()[model] || pricing_1.OpenAiTextPricing[model];
        if (!pricing) {
            return undefined;
        }
        const cachedInputTokens = ("prompt_tokens_details" in usage &&
            usage.prompt_tokens_details?.cached_tokens) ||
            0;
        const cachedInputCost = (cachedInputTokens * (pricing.cached_input ?? 0)) / 1e6;
        const inputTokens = usage.prompt_tokens;
        const inputCost = ((inputTokens - cachedInputTokens) * (pricing.input ?? 0)) / 1e6;
        const outputTokens = ("completion_tokens" in usage && usage?.completion_tokens) || 0;
        const outputCost = (outputTokens * (pricing.output ?? 0)) / 1e6;
        const total = cachedInputCost + inputCost + outputCost;
        return total;
    }
    async getModels(modality) {
        if (modality) {
            const map = {
                completion: [...new Set([...types_1.OpenAiChatModels, ...types_1.OpenAiResponsesOnlyModels])],
                embedding: types_1.OpenAiEmbeddingModelsList,
                image: types_1.OpenAiImageModels,
                audio: [...types_1.OpenAiTTSModels, ...types_1.OpenAiTranscriptionModels],
                transcription: types_1.OpenAiTranscriptionModels,
                video: types_1.OpenAiVideoModels,
            };
            return (map[modality] ?? []).map((id) => ({ id }));
        }
        const models = await this.client.models.list();
        return models.data.map((m) => ({
            id: m.id,
        }));
    }
    async createEmbedding(options) {
        const openAiEmbedding = await this.client.embeddings.create({
            input: options.input,
            model: options.model,
        });
        return {
            data: openAiEmbedding.data,
            model: options.model,
            usage: openAiEmbedding.usage,
            usd_cost: this.calculateCost(options.model, openAiEmbedding.usage),
        };
    }
    async createAudioTranscription(options) {
        let file = options.file;
        if (Buffer.isBuffer(options.file)) {
            const fileName = options.fileName || "audio.mp3";
            file = await openai_1.default.toFile(options.file, fileName);
        }
        const response = await this.client.audio.transcriptions.create({
            file,
            model: options.model || "whisper-1",
            language: options.language,
            prompt: options.prompt,
            response_format: options.response_format || "verbose_json",
            temperature: options.temperature,
        }, { signal: options.signal });
        const duration = typeof response === "object" && "duration" in response && typeof response.duration === "number"
            ? response.duration
            : undefined;
        const usdCost = duration ? (duration / 60) * 0.006 : undefined;
        if (typeof response === "string") {
            return {
                text: response,
                usd_cost: usdCost,
            };
        }
        const verboseResponse = response;
        return {
            text: response.text,
            language: verboseResponse.language,
            duration: verboseResponse.duration,
            segments: verboseResponse.segments,
            usd_cost: usdCost,
        };
    }
    async createAudioGeneration(options) {
        const response = await this.client.audio.speech.create({
            model: options.model,
            input: options.input,
            voice: options.voice,
            response_format: options.response_format || "mp3",
            speed: options.speed,
        }, { signal: options.signal });
        const buffer = Buffer.from(await response.arrayBuffer());
        const isHD = options.model.includes("hd");
        const pricePerMillion = isHD ? 30.0 : 15.0;
        const usdCost = (options.input.length * pricePerMillion) / 1e6;
        return {
            audio: buffer,
            format: options.response_format || "mp3",
            usd_cost: usdCost,
        };
    }
    async createImageGeneration(options) {
        const response = await this.client.images.generate({
            model: options.model,
            prompt: options.prompt,
            n: options.n,
            size: options.size,
            quality: options.quality,
            style: options.style,
            response_format: options.response_format,
            user: options.user,
        }, { signal: options.signal });
        const estimatedCostPerImage = options.quality === "hd" ? 0.08 : 0.04;
        const usdCost = (options.n || 1) * estimatedCostPerImage;
        return { ...response, usd_cost: usdCost };
    }
    async createVideoGeneration(options) {
        const apiKey = this.apiKey || process.env.OPENAI_KEY;
        if (!apiKey) {
            throw new Error("OpenAI API key is required for video generation");
        }
        const model = options.model || "sora-2";
        const createPayload = {
            model,
            prompt: options.prompt,
        };
        if (options.duration) {
            const validSeconds = [4, 8, 12];
            const duration = options.duration;
            const nearest = validSeconds.reduce((prev, curr) => Math.abs(curr - duration) < Math.abs(prev - duration) ? curr : prev);
            createPayload.seconds = String(nearest);
        }
        if (options.resolution) {
            createPayload.size = options.resolution;
        }
        if (options.n) {
            createPayload.n = options.n;
        }
        const createResponse = await fetch("https://api.openai.com/v1/videos", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                Authorization: `Bearer ${apiKey}`,
            },
            body: JSON.stringify(createPayload),
        });
        if (!createResponse.ok) {
            const errorText = await createResponse.text();
            throw new Error(`OpenAI video generation failed: ${createResponse.status} ${errorText}`);
        }
        const createData = await createResponse.json();
        const videoId = createData.id;
        if (!videoId) {
            throw new Error("No video ID returned from OpenAI video generation");
        }
        return {
            created: createData.created_at || Math.floor(Date.now() / 1000),
            data: [],
            jobId: videoId,
            usd_cost: undefined,
        };
    }
    async getVideoStatus(options) {
        const apiKey = this.apiKey || process.env.OPENAI_KEY;
        if (!apiKey)
            throw new Error("OpenAI API key not set");
        const response = await fetch(`https://api.openai.com/v1/videos/${options.jobId}`, {
            method: "GET",
            headers: { Authorization: `Bearer ${apiKey}` },
        });
        if (!response.ok) {
            const errorText = await response.text();
            throw new Error(`OpenAI getVideoStatus failed: ${response.status} ${errorText}`);
        }
        const data = await response.json();
        let status = "in_progress";
        if (data.status === "completed")
            status = "completed";
        else if (data.status === "failed")
            status = "failed";
        else if (data.status === "queued")
            status = "queued";
        else if (data.status === "in_progress")
            status = "in_progress";
        return {
            jobId: options.jobId,
            status,
            data: data.result?.url ? [{ url: data.result.url }] : undefined,
            error: data.error?.message,
        };
    }
    async downloadVideo(options) {
        const apiKey = this.apiKey || process.env.OPENAI_KEY;
        if (!apiKey)
            throw new Error("OpenAI API key not set");
        const fileId = options.fileId;
        if (!fileId)
            throw new Error("downloadVideo requires fileId (the jobId)");
        const response = await fetch(`https://api.openai.com/v1/videos/${fileId}/content`, {
            method: "GET",
            headers: { Authorization: `Bearer ${apiKey}` },
        });
        if (!response.ok) {
            const errorText = await response.text();
            throw new Error(`OpenAI downloadVideo failed: ${response.status} ${errorText}`);
        }
        const mimeType = response.headers.get("content-type") || "video/mp4";
        return { data: Buffer.from(await response.arrayBuffer()), mimeType };
    }
    async uploadFile(options) {
        const apiKey = this.apiKey || process.env.OPENAI_KEY;
        if (!apiKey)
            throw new Error("OpenAI API key not set");
        const formData = new FormData();
        formData.append("purpose", "assistants");
        const blob = new Blob([new Uint8Array(options.data)], { type: options.mimeType || "application/octet-stream" });
        formData.append("file", blob, options.fileName || "upload");
        const response = await fetch("https://api.openai.com/v1/files", {
            method: "POST",
            headers: { Authorization: `Bearer ${apiKey}` },
            body: formData,
        });
        if (!response.ok) {
            const errorText = await response.text();
            throw new Error(`OpenAI uploadFile failed: ${response.status} ${errorText}`);
        }
        const data = await response.json();
        return { fileId: data.id, uri: data.uri };
    }
    async downloadFile(options) {
        const apiKey = this.apiKey || process.env.OPENAI_KEY;
        if (!apiKey)
            throw new Error("OpenAI API key not set");
        const fileId = options.fileId;
        if (!fileId)
            throw new Error("downloadFile requires fileId");
        const response = await fetch(`https://api.openai.com/v1/files/${fileId}/content`, {
            method: "GET",
            headers: { Authorization: `Bearer ${apiKey}` },
        });
        if (!response.ok) {
            const errorText = await response.text();
            throw new Error(`OpenAI downloadFile failed: ${response.status} ${errorText}`);
        }
        const mimeType = response.headers.get("content-type") || undefined;
        const data = Buffer.from(await response.arrayBuffer());
        return { data, mimeType };
    }
    getContextLimit(model) {
        const contextLimit = contextLimits_1.ContextLimits[model];
        if (contextLimit === undefined)
            return undefined;
        const pricing = pricing_1.OpenAiTextPricing[model];
        const threshold = pricing && "input_gt_200k" in pricing ? 200_000 : contextLimit;
        return { contextLimit, threshold };
    }
}
exports.GenericOpenAiClient = GenericOpenAiClient;
//# sourceMappingURL=openai.js.map