import { createOpenRouter } from "@openrouter/ai-sdk-provider";
import { NoOutputGeneratedError, Output, streamText, } from "ai";
import { AIProviderName } from "../constants/enums.js";
import { BaseProvider } from "../core/baseProvider.js";
import { DEFAULT_MAX_STEPS } from "../core/constants.js";
import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
import { createProxyFetch } from "../proxy/proxyFetch.js";
import { isAbortError } from "../utils/errorHandling.js";
import { logger } from "../utils/logger.js";
import { getProviderModel } from "../utils/providerConfig.js";
import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
import { resolveToolChoice } from "../utils/toolChoice.js";
// Constants
const MODELS_DISCOVERY_TIMEOUT_MS = 5000; // 5 seconds for model discovery
// Configuration helpers
const getOpenRouterConfig = () => {
    const apiKey = process.env.OPENROUTER_API_KEY;
    if (!apiKey) {
        throw new Error("OPENROUTER_API_KEY environment variable is required. " +
            "Get your API key at https://openrouter.ai/keys");
    }
    return {
        apiKey,
        referer: process.env.OPENROUTER_REFERER,
        appName: process.env.OPENROUTER_APP_NAME,
    };
};
/**
 * Returns the default model name for OpenRouter.
 *
 * OpenRouter uses a 'provider/model' format for model names.
 * For example:
 *   - 'anthropic/claude-3-5-sonnet'
 *   - 'openai/gpt-4o'
 *   - 'google/gemini-2.0-flash'
 *   - 'meta-llama/llama-3-70b-instruct'
 *
 * You can override the default by setting the OPENROUTER_MODEL environment variable.
 */
const getDefaultOpenRouterModel = () => {
    return getProviderModel("OPENROUTER_MODEL", "anthropic/claude-3-5-sonnet");
};
/**
 * OpenRouter Provider - BaseProvider Implementation
 * Provides access to 300+ models from 60+ providers via OpenRouter unified gateway
 */
export class OpenRouterProvider extends BaseProvider {
    model;
    openRouterClient;
    // Cache for available models to avoid repeated API calls
    static modelsCache = [];
    static modelsCacheTime = 0;
    static MODELS_CACHE_DURATION = 10 * 60 * 1000; // 10 minutes
    // Cache for model capabilities (which models support tools)
    static toolCapableModels = new Set();
    static capabilitiesCached = false;
    constructor(modelName, sdk) {
        super(modelName, AIProviderName.OPENROUTER, sdk);
        // Initialize OpenRouter using the official SDK
        const config = getOpenRouterConfig();
        // Build headers for attribution on openrouter.ai/activity dashboard
        const headers = {};
        if (config.referer) {
            headers["HTTP-Referer"] = config.referer;
        }
        if (config.appName) {
            headers["X-Title"] = config.appName;
        }
        // Create OpenRouter client with optional attribution headers
        this.openRouterClient = createOpenRouter({
            apiKey: config.apiKey,
            ...(Object.keys(headers).length > 0 && { headers }),
        });
        // Initialize model with OpenRouter client
        // OpenRouterChatLanguageModel implements LanguageModelV3 which is part of the LanguageModel union
        this.model = this.openRouterClient(this.modelName || getDefaultOpenRouterModel());
        logger.debug("OpenRouter Provider initialized", {
            modelName: this.modelName,
            provider: this.providerName,
        });
    }
    getProviderName() {
        return AIProviderName.OPENROUTER;
    }
    getDefaultModel() {
        return getDefaultOpenRouterModel();
    }
    /**
     * Returns the Vercel AI SDK model instance for OpenRouter
     */
    getAISDKModel() {
        return this.model;
    }
    formatProviderError(error) {
        if (error instanceof TimeoutError) {
            return new Error(`OpenRouter request timed out: ${error.message}`);
        }
        // Check for timeout by error name and message as fallback
        const errorRecord = error;
        if (errorRecord?.name === "TimeoutError" ||
            (typeof errorRecord?.message === "string" &&
                errorRecord.message.includes("Timeout"))) {
            return new Error(`OpenRouter request timed out: ${errorRecord?.message || "Unknown timeout"}`);
        }
        if (typeof errorRecord?.message === "string") {
            if (errorRecord.message.includes("ECONNREFUSED") ||
                errorRecord.message.includes("Failed to fetch")) {
                return new Error("OpenRouter API not available. Please check your network connection and try again.");
            }
            if (errorRecord.message.includes("API_KEY_INVALID") ||
                errorRecord.message.includes("Invalid API key") ||
                errorRecord.message.includes("invalid_api_key") ||
                errorRecord.message.includes("Unauthorized")) {
                return new Error("Invalid OpenRouter API key. Please check your OPENROUTER_API_KEY environment variable. " +
                    "Get your key at https://openrouter.ai/keys");
            }
            if (errorRecord.message.includes("rate limit")) {
                return new Error("OpenRouter rate limit exceeded. Please try again later or upgrade your account at https://openrouter.ai/credits");
            }
            if (errorRecord.message.includes("model") &&
                errorRecord.message.includes("not found")) {
                return new Error(`Model '${this.modelName}' not available on OpenRouter. ` +
                    "Browse available models at https://openrouter.ai/models");
            }
            if (errorRecord.message.includes("insufficient_credits")) {
                return new Error("Insufficient OpenRouter credits. Add credits at https://openrouter.ai/credits");
            }
            // "No endpoints found" — model temporarily unavailable or unsupported parameters
            // This is distinct from tool errors: it can happen on any request when the
            // model has no available providers on OpenRouter (e.g., free-tier model down).
            if (errorRecord.message.includes("No endpoints found")) {
                return new Error(`No endpoints found for model '${this.modelName}' on OpenRouter. ` +
                    "The model may be temporarily unavailable or does not support the requested parameters. " +
                    "Try a different model or check availability at https://openrouter.ai/models");
            }
            // Tool/function calling errors
            if (errorRecord.message.includes("tool use") ||
                errorRecord.message.includes("tool_use") ||
                errorRecord.message.includes("function_call") ||
                errorRecord.message.includes("tools are not supported")) {
                return new Error(`Model '${this.modelName}' does not support tool calling. ` +
                    "Use a tool-capable model like:\n" +
                    "  • google/gemini-2.0-flash-exp:free (free)\n" +
                    "  • meta-llama/llama-3.3-70b-instruct:free (free)\n" +
                    "  • anthropic/claude-3-5-sonnet (paid)\n" +
                    "  • openai/gpt-4o (paid)\n" +
                    "Or use --disableTools flag. " +
                    "See all tool-capable models at https://openrouter.ai/models?supported_parameters=tools");
            }
        }
        return new Error(`OpenRouter error: ${errorRecord?.message || "Unknown error"}`);
    }
    /**
     * OpenRouter supports tools for compatible models
     * Checks cached model capabilities or uses known patterns as fallback
     */
    supportsTools() {
        const modelName = this.modelName || getDefaultOpenRouterModel();
        // If we have cached capabilities, use them
        if (OpenRouterProvider.capabilitiesCached) {
            const supported = OpenRouterProvider.toolCapableModels.has(modelName);
            logger.debug("OpenRouter: Tool support check (cached)", {
                model: modelName,
                supportsTools: supported,
            });
            return supported;
        }
        // Fallback: Known tool-capable model patterns (conservative list)
        const knownToolCapablePatterns = [
            "anthropic/claude",
            "openai/gpt-4",
            "openai/gpt-3.5",
            "openai/o1",
            "openai/o3",
            "openai/o4",
            "google/gemini",
            "google/gemma-3",
            "mistralai/mistral-large",
            "mistralai/mistral-small",
            "mistralai/devstral",
            "meta-llama/llama-3.3",
            "meta-llama/llama-3.2",
            "qwen/qwen3",
            "nvidia/nemotron",
        ];
        const isKnownCapable = knownToolCapablePatterns.some((pattern) => modelName.toLowerCase().includes(pattern.toLowerCase()));
        if (isKnownCapable) {
            logger.debug("OpenRouter: Tool support enabled (pattern match)", {
                model: modelName,
            });
            return true;
        }
        // For unknown models, warn and disable tools (safe default)
        logger.warn("OpenRouter: Unknown model tool capability, disabling tools", {
            model: modelName,
            suggestion: "Use a known tool-capable model like anthropic/claude-3-5-sonnet, openai/gpt-4o, or google/gemini-2.0-flash-exp:free",
        });
        return false;
    }
    /**
     * Provider-specific streaming implementation
     * Note: This is only used when tools are disabled
     */
    async executeStream(options, analysisSchema) {
        this.validateStreamOptions(options);
        const startTime = Date.now();
        let chunkCount = 0; // Track chunk count for debugging
        const timeout = this.getTimeout(options);
        const timeoutController = createTimeoutController(timeout, this.providerName, "stream");
        try {
            // Build message array from options with multimodal support
            // Using protected helper from BaseProvider to eliminate code duplication
            const messages = await this.buildMessagesForStream(options);
            const model = await this.getAISDKModelWithMiddleware(options);
            // Get all available tools (direct + MCP + external) for streaming
            // BaseProvider.stream() pre-merges base tools + external tools into options.tools
            const shouldUseTools = !options.disableTools && this.supportsTools();
            const tools = shouldUseTools
                ? options.tools || (await this.getAllTools())
                : {};
            logger.debug(`OpenRouter: Tools for streaming`, {
                shouldUseTools,
                toolCount: Object.keys(tools).length,
                toolNames: Object.keys(tools),
            });
            // Build complete stream options with proper typing
            // Note: maxRetries set to 0 for OpenRouter free tier to prevent SDK's quick retries
            // from consuming rate limits. Our test suite handles retries with appropriate delays.
            let streamOptions = {
                model: model,
                messages: messages,
                temperature: options.temperature,
                maxRetries: 0, // Disable SDK retries - let caller handle rate limit retries with delays
                ...(options.maxTokens && { maxTokens: options.maxTokens }),
                ...(shouldUseTools &&
                    Object.keys(tools).length > 0 && {
                    tools,
                    toolChoice: resolveToolChoice(options, tools, shouldUseTools),
                    maxSteps: options.maxSteps || DEFAULT_MAX_STEPS,
                }),
                abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
                experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
                onError: (event) => {
                    const error = event.error;
                    const errorMessage = error instanceof Error ? error.message : String(error);
                    logger.error(`OpenRouter: Stream error`, {
                        provider: this.providerName,
                        modelName: this.modelName,
                        error: errorMessage,
                        chunkCount,
                    });
                },
                onFinish: (event) => {
                    logger.debug(`OpenRouter: Stream finished`, {
                        finishReason: event.finishReason,
                        totalChunks: chunkCount,
                    });
                },
                onChunk: () => {
                    chunkCount++;
                },
                onStepFinish: ({ toolCalls, toolResults }) => {
                    logger.info("Tool execution completed", {
                        toolCallCount: toolCalls?.length || 0,
                        toolResultCount: toolResults?.length || 0,
                        toolNames: toolCalls?.map((tc) => tc.toolName),
                    });
                    this.handleToolExecutionStorage(toolCalls, toolResults, options, new Date()).catch((error) => {
                        logger.warn("OpenRouterProvider: Failed to store tool executions", {
                            provider: this.providerName,
                            error: error instanceof Error ? error.message : String(error),
                        });
                    });
                },
            };
            // Add analysisSchema support if provided
            if (analysisSchema) {
                try {
                    streamOptions = {
                        ...streamOptions,
                        experimental_output: Output.object({
                            schema: analysisSchema,
                        }),
                    };
                }
                catch (error) {
                    logger.warn("Schema application failed, continuing without schema", {
                        error: String(error),
                    });
                }
            }
            const result = await streamText(streamOptions);
            // Guard against NoOutputGeneratedError becoming an unhandled rejection.
            Promise.resolve(result.text)
                .catch((err) => {
                logger.debug("Stream text promise rejected (expected for empty streams)", {
                    error: err instanceof Error ? err.message : String(err),
                });
            })
                .finally(() => timeoutController?.cleanup());
            // Transform stream to content object stream using fullStream (handles both text and tool calls)
            const transformedStream = (async function* () {
                try {
                    // Try fullStream first (handles both text and tool calls), fallback to textStream
                    const streamToUse = result.fullStream || result.textStream;
                    for await (const chunk of streamToUse) {
                        // Handle different chunk types from fullStream
                        if (chunk && typeof chunk === "object") {
                            // Check for error chunks first (critical error handling)
                            if ("type" in chunk && chunk.type === "error") {
                                const errorChunk = chunk;
                                logger.error(`OpenRouter: Error chunk received:`, {
                                    errorType: errorChunk.type,
                                    errorDetails: errorChunk.error,
                                });
                                throw new Error(`OpenRouter streaming error: ${errorChunk.error?.message ||
                                    "Unknown error"}`);
                            }
                            if ("textDelta" in chunk) {
                                // Text delta from fullStream
                                const textDelta = chunk.textDelta;
                                if (textDelta) {
                                    yield { content: textDelta };
                                }
                            }
                            else if ("type" in chunk &&
                                chunk.type === "tool-call" &&
                                "toolCallId" in chunk) {
                                // Tool call event - log for debugging
                                const toolCallId = String(chunk.toolCallId);
                                const toolName = "toolName" in chunk ? String(chunk.toolName) : "unknown";
                                logger.debug("OpenRouter: Tool call", {
                                    toolCallId,
                                    toolName,
                                });
                            }
                        }
                        else if (typeof chunk === "string") {
                            // Direct string chunk from textStream fallback
                            yield { content: chunk };
                        }
                    }
                }
                catch (streamError) {
                    // AI SDK v6 throws NoOutputGeneratedError when the stream produced no output.
                    if (NoOutputGeneratedError.isInstance(streamError)) {
                        logger.warn("OpenRouter: Stream produced no output (NoOutputGeneratedError)");
                        return;
                    }
                    throw streamError;
                }
            })();
            // Create analytics promise that resolves after stream completion
            const analyticsPromise = streamAnalyticsCollector.createAnalytics(this.providerName, this.modelName, result, Date.now() - startTime, {
                requestId: `openrouter-stream-${Date.now()}`,
                streamingMode: true,
            });
            return {
                stream: transformedStream,
                provider: this.providerName,
                model: this.modelName,
                analytics: analyticsPromise,
                metadata: {
                    startTime,
                    streamId: `openrouter-${Date.now()}`,
                },
            };
        }
        catch (error) {
            timeoutController?.cleanup();
            throw this.handleProviderError(error);
        }
    }
    /**
     * Get available models from OpenRouter API
     * Dynamically fetches from /api/v1/models endpoint with caching and fallback
     */
    async getAvailableModels() {
        const functionTag = "OpenRouterProvider.getAvailableModels";
        const now = Date.now();
        // Check if cached models are still valid
        if (OpenRouterProvider.modelsCache.length > 0 &&
            now - OpenRouterProvider.modelsCacheTime <
                OpenRouterProvider.MODELS_CACHE_DURATION) {
            logger.debug(`[${functionTag}] Using cached models`, {
                cacheAge: Math.round((now - OpenRouterProvider.modelsCacheTime) / 1000),
                modelCount: OpenRouterProvider.modelsCache.length,
            });
            return OpenRouterProvider.modelsCache;
        }
        // Try to fetch models dynamically
        try {
            const dynamicModels = await this.fetchModelsFromAPI();
            if (dynamicModels.length > 0) {
                // Cache successful result
                OpenRouterProvider.modelsCache = dynamicModels;
                OpenRouterProvider.modelsCacheTime = now;
                logger.debug(`[${functionTag}] Successfully fetched models from API`, {
                    modelCount: dynamicModels.length,
                });
                return dynamicModels;
            }
        }
        catch (error) {
            logger.warn(`[${functionTag}] Failed to fetch models from API, using fallback`, {
                error: error instanceof Error ? error.message : String(error),
            });
        }
        // Fallback to hardcoded list if API fetch fails
        const fallbackModels = [
            // Anthropic Claude models
            "anthropic/claude-3-5-sonnet",
            "anthropic/claude-3-5-haiku",
            "anthropic/claude-3-opus",
            // OpenAI models
            "openai/gpt-4o",
            "openai/gpt-4o-mini",
            "openai/gpt-4-turbo",
            // Google models
            "google/gemini-2.0-flash",
            "google/gemini-1.5-pro",
            // Meta Llama models
            "meta-llama/llama-3.1-70b-instruct",
            "meta-llama/llama-3.1-8b-instruct",
            // Mistral models
            "mistralai/mistral-large",
            "mistralai/mixtral-8x7b-instruct",
        ];
        logger.debug(`[${functionTag}] Using fallback model list`, {
            modelCount: fallbackModels.length,
        });
        return fallbackModels;
    }
    /**
     * Fetch available models from OpenRouter API /api/v1/models endpoint
     * @private
     */
    async fetchModelsFromAPI() {
        const functionTag = "OpenRouterProvider.fetchModelsFromAPI";
        const config = getOpenRouterConfig();
        const modelsUrl = "https://openrouter.ai/api/v1/models";
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), MODELS_DISCOVERY_TIMEOUT_MS);
        try {
            logger.debug(`[${functionTag}] Fetching models from ${modelsUrl}`);
            const proxyFetch = createProxyFetch();
            const response = await proxyFetch(modelsUrl, {
                method: "GET",
                headers: {
                    Authorization: `Bearer ${config.apiKey}`,
                    "Content-Type": "application/json",
                },
                signal: controller.signal,
            });
            clearTimeout(timeoutId);
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }
            const data = await response.json();
            // Parse OpenRouter models response with type guard
            if (!this.isValidModelsResponse(data)) {
                throw new Error("Invalid response format: expected data.data array");
            }
            const models = data.data
                .map((model) => model.id)
                .filter((id) => typeof id === "string" && id.length > 0)
                .sort();
            logger.debug(`[${functionTag}] Successfully parsed models`, {
                totalModels: models.length,
                sampleModels: models.slice(0, 5),
            });
            return models;
        }
        catch (error) {
            clearTimeout(timeoutId);
            if (isAbortError(error)) {
                throw new Error(`Request timed out after ${MODELS_DISCOVERY_TIMEOUT_MS / 1000} seconds`, { cause: error });
            }
            throw error;
        }
    }
    /**
     * Type guard to validate the models API response structure
     * @private
     */
    isValidModelsResponse(data) {
        return (data !== null &&
            typeof data === "object" &&
            "data" in data &&
            Array.isArray(data.data));
    }
    /**
     * Fetch and cache model capabilities from OpenRouter API
     * Call this to enable accurate tool support detection
     */
    async cacheModelCapabilities() {
        const functionTag = "OpenRouterProvider.cacheModelCapabilities";
        if (OpenRouterProvider.capabilitiesCached) {
            return; // Already cached
        }
        try {
            const config = getOpenRouterConfig();
            const modelsUrl = "https://openrouter.ai/api/v1/models";
            const controller = new AbortController();
            const timeoutId = setTimeout(() => controller.abort(), MODELS_DISCOVERY_TIMEOUT_MS);
            const proxyFetch = createProxyFetch();
            const response = await proxyFetch(modelsUrl, {
                method: "GET",
                headers: {
                    Authorization: `Bearer ${config.apiKey}`,
                    "Content-Type": "application/json",
                },
                signal: controller.signal,
            });
            clearTimeout(timeoutId);
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}: ${response.statusText}`);
            }
            const data = await response.json();
            if (!this.isValidModelsResponse(data)) {
                throw new Error("Invalid response format");
            }
            // Extract tool-capable models
            const toolCapable = new Set();
            for (const model of data.data) {
                if (model.id && model.supported_parameters?.includes("tools")) {
                    toolCapable.add(model.id);
                }
            }
            OpenRouterProvider.toolCapableModels = toolCapable;
            OpenRouterProvider.capabilitiesCached = true;
            logger.debug(`[${functionTag}] Cached model capabilities`, {
                totalModels: data.data.length,
                toolCapableCount: toolCapable.size,
            });
        }
        catch (error) {
            logger.warn(`[${functionTag}] Failed to cache capabilities, using fallback patterns`, {
                error: error instanceof Error ? error.message : String(error),
            });
            // Don't set capabilitiesCached - let it use fallback patterns
        }
    }
}
//# sourceMappingURL=openRouter.js.map