/**
 * provider.ts
 *
 * Instrumented Promptfoo custom provider that wraps any underlying LLM
 * provider and records all HTTP activity the agent performs during a task.
 *
 * This is the integration point between the observation infrastructure
 * (RequestRecorder) and Promptfoo's evaluation pipeline. It:
 *
 *   1. Starts a recording session before the LLM call
 *   2. Delegates to the real provider (calls OpenAI directly)
 *   3. Stops recording and attaches the AgentBehaviorLog to
 *      ProviderResponse.metadata so it flows through scoring
 *
 * Promptfoo config usage:
 *
 *   providers:
 *     - id: file://src/agent-observer/provider.ts
 *       label: "gpt-4o (observed)"
 *       config:
 *         model: gpt-4o
 *         temperature: 0
 *         max_tokens: 4096
 *         observe: true
 *         observerOptions:
 *           maxPreviewBytes: 2048
 *           captureResponsePreview: true
 *
 * Promptfoo loads this file and instantiates the default export class.
 */
import { config as loadDotenv } from "dotenv";
import { randomUUID } from "crypto";
import { OpenAIChatResponseSchema, OpenAIResponsesResponseSchema, } from "../_vendor/ailf-core/index.js";
import { InstrumentedProviderConfigSchema, } from "./config-schemas.js";
import { RequestRecorder } from "./proxy.js";
import { calculateCost } from "./pricing.js";
loadDotenv({
    override: true,
    path: new URL("../../.env", import.meta.url).pathname,
});
// ---------------------------------------------------------------------------
// Provider implementation
// ---------------------------------------------------------------------------
export default class InstrumentedProvider {
    config;
    providerId;
    parsedConfig;
    recorder;
    constructor(options) {
        this.providerId = options.id ?? "instrumented-observer";
        this.config = options.config ?? {};
        this.parsedConfig = InstrumentedProviderConfigSchema.parse(this.config);
        this.recorder = new RequestRecorder(this.parsedConfig.observerOptions ?? {});
    }
    /**
     * Main Promptfoo provider entry point. Called for each test case.
     */
    async callApi(prompt, context) {
        const sessionId = randomUUID();
        const taskDescription = context?.vars?.task ||
            context?.prompt?.label ||
            "unknown-task";
        const observe = this.parsedConfig.observe !== false;
        // Start observation
        if (observe) {
            this.recorder.start(sessionId, this.id(), taskDescription);
        }
        let result;
        try {
            result = await this.callOpenAI(prompt);
        }
        catch (err) {
            const error = err;
            result = {
                error: error.message,
                output: undefined,
            };
        }
        // Stop observation and attach behavior log
        if (observe) {
            const behaviorLog = this.recorder.stop();
            result.metadata = {
                ...(result.metadata ?? {}),
                agentBehavior: behaviorLog,
                agentBehaviorSummary: behaviorLog.summary,
            };
        }
        return result;
    }
    /**
     * Exposes the recorder for external integrations (e.g., agents that
     * make their own HTTP requests outside the provider call).
     */
    getRecorder() {
        return this.recorder;
    }
    id() {
        return `instrumented:${this.providerId}`;
    }
    /**
     * Detect whether the model should use the Responses API based on config.
     */
    isResponsesModel() {
        const model = this.parsedConfig.modelName ?? this.parsedConfig.model ?? "";
        return (this.parsedConfig.reasoning != null ||
            this.parsedConfig.reasoning_effort != null ||
            model.startsWith("gpt-5") ||
            model.startsWith("o1") ||
            model.startsWith("o3") ||
            model.startsWith("o4"));
    }
    /**
     * Calls OpenAI directly. Routes to the Responses API for reasoning
     * models and Chat Completions API for everything else. Uses the
     * recorder's fetch wrapper so the API call is captured.
     */
    async callOpenAI(prompt) {
        const model = this.parsedConfig.modelName ?? this.parsedConfig.model ?? "gpt-4o";
        const apiKey = this.parsedConfig.apiKey ?? process.env.OPENAI_API_KEY;
        if (!apiKey) {
            return {
                error: "OPENAI_API_KEY not set. Configure it in env or provider config.",
                output: undefined,
            };
        }
        if (this.isResponsesModel()) {
            return this.callOpenAIResponses(prompt, model, apiKey);
        }
        return this.callOpenAIChatCompletions(prompt, model, apiKey);
    }
    /**
     * Calls OpenAI Chat Completions API (non-reasoning models).
     */
    async callOpenAIChatCompletions(prompt, model, apiKey) {
        const temperature = this.parsedConfig.temperature ?? 0;
        const maxTokens = this.parsedConfig.max_tokens ?? 4096;
        const fetchFn = this.recorder.isRunning()
            ? this.recorder.fetch.bind(this.recorder)
            : globalThis.fetch;
        const startTime = Date.now();
        const response = await fetchFn("https://api.openai.com/v1/chat/completions", {
            body: JSON.stringify({
                max_tokens: maxTokens,
                messages: [{ content: prompt, role: "user" }],
                model,
                temperature,
            }),
            headers: {
                Authorization: `Bearer ${apiKey}`,
                "Content-Type": "application/json",
            },
            method: "POST",
        });
        const data = OpenAIChatResponseSchema.parse(await response.json());
        if (data.error) {
            return {
                error: data.error.message ?? "Unknown OpenAI error",
                output: undefined,
            };
        }
        const output = data.choices?.[0]?.message?.content ?? "";
        return {
            cost: calculateCost(model, data.usage?.prompt_tokens ?? 0, data.usage?.completion_tokens ?? 0),
            metadata: {
                latencyMs: Date.now() - startTime,
                model,
            },
            output,
            tokenUsage: {
                completion: data.usage?.completion_tokens,
                prompt: data.usage?.prompt_tokens,
                total: data.usage?.total_tokens,
            },
        };
    }
    /**
     * Calls OpenAI Responses API (reasoning models like GPT-5.x, o-series).
     */
    async callOpenAIResponses(prompt, model, apiKey) {
        const maxOutputTokens = this.parsedConfig.max_output_tokens ?? 32_000;
        const reasoning = this.parsedConfig.reasoning;
        const reasoningEffort = reasoning?.effort ?? this.parsedConfig.reasoning_effort ?? "medium";
        const reasoningSummary = reasoning?.summary;
        const verbosity = this.parsedConfig.verbosity;
        const fetchFn = this.recorder.isRunning()
            ? this.recorder.fetch.bind(this.recorder)
            : globalThis.fetch;
        const startTime = Date.now();
        const response = await fetchFn("https://api.openai.com/v1/responses", {
            body: JSON.stringify({
                input: prompt,
                max_output_tokens: maxOutputTokens,
                model,
                reasoning: {
                    effort: reasoningEffort,
                    ...(reasoningSummary ? { summary: reasoningSummary } : {}),
                },
                ...(verbosity ? { text: { format: { type: "text" }, verbosity } } : {}),
            }),
            headers: {
                Authorization: `Bearer ${apiKey}`,
                "Content-Type": "application/json",
            },
            method: "POST",
        });
        const data = OpenAIResponsesResponseSchema.parse(await response.json());
        if (data.error) {
            return {
                error: data.error.message ?? "Unknown OpenAI error",
                output: undefined,
            };
        }
        // Extract text from Responses API output format
        let output = "";
        for (const item of data.output ?? []) {
            if (item.type === "message" && item.content) {
                for (const block of item.content) {
                    if (block.type === "output_text" && block.text) {
                        output += block.text;
                    }
                }
            }
        }
        return {
            cost: calculateCost(model, data.usage?.input_tokens ?? 0, data.usage?.output_tokens ?? 0),
            metadata: {
                latencyMs: Date.now() - startTime,
                model,
            },
            output,
            tokenUsage: {
                completion: data.usage?.output_tokens,
                prompt: data.usage?.input_tokens,
                total: data.usage?.total_tokens,
            },
        };
    }
}
