const { fetchOpenRouterModels } = require("../AiProviders/openRouter");
const {
  fetchOpenRouterEmbeddingModels,
} = require("../EmbeddingEngines/openRouter");
const { fetchApiPieModels } = require("../AiProviders/apipie");
const { perplexityModels } = require("../AiProviders/perplexity");
const { fireworksAiModels } = require("../AiProviders/fireworksAi");
const { ElevenLabsTTS } = require("../TextToSpeech/elevenLabs");
const { fetchNovitaModels } = require("../AiProviders/novita");
const { parseLMStudioBasePath } = require("../AiProviders/lmStudio");
const { parseNvidiaNimBasePath } = require("../AiProviders/nvidiaNim");
const { fetchPPIOModels } = require("../AiProviders/ppio");
const { GeminiLLM } = require("../AiProviders/gemini");
const { fetchCometApiModels } = require("../AiProviders/cometapi");
const { parseFoundryBasePath } = require("../AiProviders/foundry");
const { getDockerModels } = require("../AiProviders/dockerModelRunner");
const { getAllLemonadeModels } = require("../AiProviders/lemonade");

const SUPPORT_CUSTOM_MODELS = [
  "openai",
  "anthropic",
  "localai",
  "ollama",
  "togetherai",
  "fireworksai",
  "nvidia-nim",
  "mistral",
  "perplexity",
  "openrouter",
  "lmstudio",
  "koboldcpp",
  "litellm",
  "elevenlabs-tts",
  "groq",
  "deepseek",
  "apipie",
  "novita",
  "cometapi",
  "xai",
  "gemini",
  "ppio",
  "moonshotai",
  "foundry",
  "cohere",
  "zai",
  "giteeai",
  "docker-model-runner",
  "privatemode",
  "sambanova",
  "lemonade",
  "minimax",
  "cerebras",
  "bedrock",
  "generic-openai",
  // Embedding Engines
  "native-embedder",
  "cohere-embedder",
  "openrouter-embedder",
  "lemonade-embedder",
  // STT Engines
  "openai-stt",
  "deepgram-stt",
  "lemonade-stt",
  "groq-stt",
  // TTS Engines
  "kokoro-tts",
];

async function getCustomModels(
  provider = "",
  apiKey = null,
  basePath = null,
  options = {}
) {
  if (!SUPPORT_CUSTOM_MODELS.includes(provider))
    return { models: [], error: "Invalid provider for custom models" };

  switch (provider) {
    case "openai":
      return await openAiModels(apiKey);
    case "openai-stt":
      return await openAiSttModels(apiKey);
    case "anthropic":
      return await anthropicModels(apiKey);
    case "localai":
      return await localAIModels(basePath, apiKey);
    case "ollama":
      return await ollamaAIModels(basePath, apiKey);
    case "togetherai":
      return await getTogetherAiModels(apiKey);
    case "fireworksai":
      return await getFireworksAiModels(apiKey);
    case "mistral":
      return await getMistralModels(apiKey);
    case "perplexity":
      return await getPerplexityModels();
    case "openrouter":
      return await getOpenRouterModels();
    case "lmstudio":
      return await getLMStudioModels(basePath, apiKey);
    case "koboldcpp":
      return await getKoboldCPPModels(basePath);
    case "litellm":
      return await liteLLMModels(basePath, apiKey);
    case "elevenlabs-tts":
      return await getElevenLabsModels(apiKey);
    case "groq":
      return await getGroqAiModels(apiKey);
    case "deepseek":
      return await getDeepSeekModels(apiKey);
    case "apipie":
      return await getAPIPieModels(apiKey);
    case "novita":
      return await getNovitaModels();
    case "cometapi":
      return await getCometApiModels();
    case "xai":
      return await getXAIModels(apiKey);
    case "nvidia-nim":
      return await getNvidiaNimModels(basePath);
    case "gemini":
      return await getGeminiModels(apiKey);
    case "ppio":
      return await getPPIOModels(apiKey);
    case "moonshotai":
      return await getMoonshotAiModels(apiKey);
    case "foundry":
      return await getFoundryModels(basePath);
    case "cohere":
      return await getCohereModels(apiKey, "chat");
    case "zai":
      return await getZAiModels(apiKey);
    case "native-embedder":
      return await getNativeEmbedderModels();
    case "cohere-embedder":
      return await getCohereModels(apiKey, "embed");
    case "openrouter-embedder":
      return await getOpenRouterEmbeddingModels();
    case "giteeai":
      return await getGiteeAIModels(apiKey);
    case "docker-model-runner":
      return await getDockerModelRunnerModels(basePath);
    case "privatemode":
      return await getPrivatemodeModels(basePath, "generate");
    case "sambanova":
      return await getSambaNovaModels(apiKey);
    case "lemonade":
      return await getLemonadeModels(basePath);
    case "lemonade-stt":
      return await getLemonadeSTTModels(basePath);
    case "lemonade-embedder":
      return await getLemonadeModels(basePath, "embedding");
    case "minimax":
      return await getMinimaxModels(apiKey);
    case "cerebras":
      return await getCerebrasModels();
    case "bedrock":
      return await getBedrockModels(apiKey, options);
    case "generic-openai":
      return await getGenericOpenAiModels(basePath, apiKey);
    case "deepgram-stt":
      return await getDeepgramSTTModels(apiKey);
    case "groq-stt":
      return await getGroqSTTModels(apiKey);
    case "kokoro-tts":
      return await kokoroTtsVoices(basePath, apiKey);
    default:
      return { models: [], error: "Invalid provider for custom models" };
  }
}

async function openAiModels(apiKey = null) {
  const { OpenAI: OpenAIApi } = require("openai");
  const openai = new OpenAIApi({
    apiKey: apiKey || process.env.OPEN_AI_KEY,
  });
  const allModels = await openai.models
    .list()
    .then((results) => results.data)
    .catch((e) => {
      console.error(`OpenAI:listModels`, e.message);
      return [
        {
          name: "gpt-3.5-turbo",
          id: "gpt-3.5-turbo",
          object: "model",
          created: 1677610602,
          owned_by: "openai",
          organization: "OpenAi",
        },
        {
          name: "gpt-4o",
          id: "gpt-4o",
          object: "model",
          created: 1677610602,
          owned_by: "openai",
          organization: "OpenAi",
        },
        {
          name: "gpt-4",
          id: "gpt-4",
          object: "model",
          created: 1687882411,
          owned_by: "openai",
          organization: "OpenAi",
        },
        {
          name: "gpt-4-turbo",
          id: "gpt-4-turbo",
          object: "model",
          created: 1712361441,
          owned_by: "system",
          organization: "OpenAi",
        },
        {
          name: "gpt-4-32k",
          id: "gpt-4-32k",
          object: "model",
          created: 1687979321,
          owned_by: "openai",
          organization: "OpenAi",
        },
        {
          name: "gpt-3.5-turbo-16k",
          id: "gpt-3.5-turbo-16k",
          object: "model",
          created: 1683758102,
          owned_by: "openai-internal",
          organization: "OpenAi",
        },
      ];
    });

  const gpts = allModels
    .filter(
      (model) =>
        (model.id.includes("gpt") && !model.id.startsWith("ft:")) ||
        model.id.startsWith("o") // o1, o1-mini, o3, etc
    )
    .filter(
      (model) =>
        !model.id.includes("vision") &&
        !model.id.includes("instruct") &&
        !model.id.includes("audio") &&
        !model.id.includes("realtime") &&
        !model.id.includes("image") &&
        !model.id.includes("moderation") &&
        !model.id.includes("transcribe")
    )
    .map((model) => {
      return {
        ...model,
        name: model.id,
        organization: "OpenAi",
      };
    });

  const customModels = allModels
    .filter(
      (model) =>
        !model.owned_by.includes("openai") && model.owned_by !== "system"
    )
    .map((model) => {
      return {
        ...model,
        name: model.id,
        organization: "Your Fine-Tunes",
      };
    });

  // Api Key was successful so lets save it for future uses
  if ((gpts.length > 0 || customModels.length > 0) && !!apiKey)
    process.env.OPEN_AI_KEY = apiKey;
  return { models: [...gpts, ...customModels], error: null };
}

async function openAiSttModels(apiKey = null) {
  const fallback = [
    { id: "whisper-1", name: "whisper-1", organization: "OpenAi" },
    {
      id: "gpt-4o-transcribe",
      name: "gpt-4o-transcribe",
      organization: "OpenAi",
    },
    {
      id: "gpt-4o-mini-transcribe",
      name: "gpt-4o-mini-transcribe",
      organization: "OpenAi",
    },
  ];

  const { OpenAI: OpenAIApi } = require("openai");
  const openai = new OpenAIApi({
    apiKey: apiKey || process.env.OPEN_AI_KEY,
  });

  const allModels = await openai.models
    .list()
    .then((results) => results.data)
    .catch((e) => {
      console.error(`OpenAI:listModels (stt)`, e.message);
      return null;
    });

  if (!allModels) return { models: fallback, error: null };

  // The /v1/models response has no category/type field, so we filter by id.
  // Realtime variants use a separate WebSocket API and are not compatible
  // with the audio.transcriptions.create endpoint we use server-side.
  const models = allModels
    .filter(
      (m) =>
        (m.id.includes("whisper") || m.id.includes("transcribe")) &&
        !m.id.includes("realtime")
    )
    .map((m) => ({ ...m, name: m.id, organization: "OpenAi" }));

  return { models: models.length ? models : fallback, error: null };
}

async function anthropicModels(_apiKey = null) {
  const apiKey =
    _apiKey === true
      ? process.env.ANTHROPIC_API_KEY
      : _apiKey || process.env.ANTHROPIC_API_KEY || null;
  const AnthropicAI = require("@anthropic-ai/sdk");
  const anthropic = new AnthropicAI({ apiKey });
  const models = await anthropic.models
    .list()
    .then((results) => results.data)
    .then((models) => {
      return models
        .filter((model) => model.type === "model")
        .map((model) => {
          return {
            id: model.id,
            name: model.display_name,
          };
        });
    })
    .catch((e) => {
      console.error(`Anthropic:listModels`, e.message);
      return [];
    });

  // Api Key was successful so lets save it for future uses
  if (models.length > 0 && !!apiKey) process.env.ANTHROPIC_API_KEY = apiKey;
  return { models, error: null };
}

async function localAIModels(basePath = null, apiKey = null) {
  const { OpenAI: OpenAIApi } = require("openai");
  const openai = new OpenAIApi({
    baseURL: basePath || process.env.LOCAL_AI_BASE_PATH,
    apiKey: apiKey || process.env.LOCAL_AI_API_KEY || null,
  });
  const models = await openai.models
    .list()
    .then((results) => results.data)
    .catch((e) => {
      console.error(`LocalAI:listModels`, e.message);
      return [];
    });

  // Api Key was successful so lets save it for future uses
  if (models.length > 0 && !!apiKey) process.env.LOCAL_AI_API_KEY = apiKey;
  return { models, error: null };
}

async function getGroqAiModels(_apiKey = null) {
  const { OpenAI: OpenAIApi } = require("openai");
  const apiKey =
    _apiKey === true
      ? process.env.GROQ_API_KEY
      : _apiKey || process.env.GROQ_API_KEY || null;
  const openai = new OpenAIApi({
    baseURL: "https://api.groq.com/openai/v1",
    apiKey,
  });
  const models = (
    await openai.models
      .list()
      .then((results) => results.data)
      .catch((e) => {
        console.error(`GroqAi:listModels`, e.message);
        return [];
      })
  ).filter(
    (model) => !model.id.includes("whisper") && !model.id.includes("tool-use")
  );

  // Api Key was successful so lets save it for future uses
  if (models.length > 0 && !!apiKey) process.env.GROQ_API_KEY = apiKey;
  return { models, error: null };
}

async function getGroqSTTModels(_apiKey = null) {
  const { OpenAI: OpenAIApi } = require("openai");
  const apiKey =
    _apiKey === true
      ? process.env.STT_GROQ_API_KEY
      : _apiKey || process.env.STT_GROQ_API_KEY || null;

  const openai = new OpenAIApi({
    baseURL: "https://api.groq.com/openai/v1",
    apiKey,
  });
  const models = (
    await openai.models
      .list()
      .then((results) => results.data)
      .catch((e) => {
        console.error(`GroqSTT:listModels`, e.message);
        return [];
      })
  ).filter((model) => model.id.includes("whisper"));

  // Api Key was successful so lets save it for future uses
  if (models.length > 0 && !!apiKey) process.env.GROQ_STT_API_KEY = apiKey;
  return { models, error: null };
}

async function liteLLMModels(basePath = null, apiKey = null) {
  const { OpenAI: OpenAIApi } = require("openai");
  const openai = new OpenAIApi({
    baseURL: basePath || process.env.LITE_LLM_BASE_PATH,
    apiKey: apiKey || process.env.LITE_LLM_API_KEY || null,
  });
  const models = await openai.models
    .list()
    .then((results) => results.data)
    .catch((e) => {
      console.error(`LiteLLM:listModels`, e.message);
      return [];
    });

  // Api Key was successful so lets save it for future uses
  if (models.length > 0 && !!apiKey) process.env.LITE_LLM_API_KEY = apiKey;
  return { models, error: null };
}

async function getLMStudioModels(basePath = null, _apiKey = null) {
  try {
    const apiKey =
      _apiKey === true
        ? process.env.LMSTUDIO_AUTH_TOKEN
        : _apiKey || process.env.LMSTUDIO_AUTH_TOKEN || null;

    const { OpenAI: OpenAIApi } = require("openai");
    const openai = new OpenAIApi({
      baseURL: parseLMStudioBasePath(
        basePath || process.env.LMSTUDIO_BASE_PATH
      ),
      apiKey: apiKey || null,
    });
    const models = await openai.models
      .list()
      .then((results) => results.data)
      .catch((e) => {
        console.error(`LMStudio:listModels`, e.message);
        return [];
      });

    return { models, error: null };
  } catch (e) {
    console.error(`LMStudio:getLMStudioModels`, e.message);
    return { models: [], error: "Could not fetch LMStudio Models" };
  }
}

async function getKoboldCPPModels(basePath = null) {
  try {
    const { OpenAI: OpenAIApi } = require("openai");
    const openai = new OpenAIApi({
      baseURL: basePath || process.env.KOBOLD_CPP_BASE_PATH,
      apiKey: null,
    });
    const models = await openai.models
      .list()
      .then((results) => results.data)
      .catch((e) => {
        console.error(`KoboldCPP:listModels`, e.message);
        return [];
      });

    return { models, error: null };
  } catch (e) {
    console.error(`KoboldCPP:getKoboldCPPModels`, e.message);
    return { models: [], error: "Could not fetch KoboldCPP Models" };
  }
}

async function ollamaAIModels(basePath = null, _authToken = null) {
  let url;
  try {
    let urlPath = basePath ?? process.env.OLLAMA_BASE_PATH;
    new URL(urlPath);
    if (urlPath.split("").slice(-1)?.[0] === "/")
      throw new Error("BasePath Cannot end in /!");
    url = urlPath;
  } catch {
    return { models: [], error: "Not a valid URL." };
  }

  const authToken = _authToken || process.env.OLLAMA_AUTH_TOKEN || null;
  const headers = authToken ? { Authorization: `Bearer ${authToken}` } : {};
  const models = await fetch(`${url}/api/tags`, { headers: headers })
    .then((res) => {
      if (!res.ok)
        throw new Error(`Could not reach Ollama server! ${res.status}`);
      return res.json();
    })
    .then((data) => data?.models || [])
    .then((models) =>
      models.map((model) => {
        return { id: model.name };
      })
    )
    .catch((e) => {
      console.error(e);
      return [];
    });

  // Api Key was successful so lets save it for future uses
  if (models.length > 0 && !!authToken)
    process.env.OLLAMA_AUTH_TOKEN = authToken;
  return { models, error: null };
}

async function getTogetherAiModels(apiKey = null) {
  const _apiKey =
    apiKey === true
      ? process.env.TOGETHER_AI_API_KEY
      : apiKey || process.env.TOGETHER_AI_API_KEY || null;
  try {
    const { togetherAiModels } = require("../AiProviders/togetherAi");
    const models = await togetherAiModels(_apiKey);
    if (models.length > 0 && !!_apiKey)
      process.env.TOGETHER_AI_API_KEY = _apiKey;
    return { models, error: null };
  } catch (error) {
    console.error("Error in getTogetherAiModels:", error);
    return { models: [], error: "Failed to fetch Together AI models" };
  }
}

async function getFireworksAiModels(apiKey = null) {
  const knownModels = await fireworksAiModels(apiKey);
  if (!Object.keys(knownModels).length === 0)
    return { models: [], error: null };

  const models = Object.values(knownModels).map((model) => {
    return {
      id: model.id,
      organization: model.organization,
      name: model.name,
    };
  });
  return { models, error: null };
}

async function getPerplexityModels() {
  const knownModels = perplexityModels();
  if (!Object.keys(knownModels).length === 0)
    return { models: [], error: null };

  const models = Object.values(knownModels).map((model) => {
    return {
      id: model.id,
      name: model.name,
    };
  });
  return { models, error: null };
}

async function getOpenRouterModels() {
  const knownModels = await fetchOpenRouterModels();
  if (!Object.keys(knownModels).length === 0)
    return { models: [], error: null };

  const models = Object.values(knownModels).map((model) => {
    return {
      id: model.id,
      organization: model.organization,
      name: model.name,
    };
  });
  return { models, error: null };
}

async function getNovitaModels() {
  const knownModels = await fetchNovitaModels();
  if (!Object.keys(knownModels).length === 0)
    return { models: [], error: null };
  const models = Object.values(knownModels).map((model) => {
    return {
      id: model.id,
      organization: model.organization,
      name: model.name,
    };
  });
  return { models, error: null };
}

async function getCometApiModels() {
  const knownModels = await fetchCometApiModels();
  if (!Object.keys(knownModels).length === 0)
    return { models: [], error: null };
  const models = Object.values(knownModels).map((model) => {
    return {
      id: model.id,
      organization: model.organization,
      name: model.name,
    };
  });
  return { models, error: null };
}

async function getAPIPieModels(apiKey = null) {
  const knownModels = await fetchApiPieModels(apiKey);
  if (!Object.keys(knownModels).length === 0)
    return { models: [], error: null };

  const models = Object.values(knownModels)
    .filter((model) => {
      // Filter for chat models
      return (
        model.subtype &&
        (model.subtype.includes("chat") || model.subtype.includes("chatx"))
      );
    })
    .map((model) => {
      return {
        id: model.id,
        organization: model.organization,
        name: model.name,
      };
    });
  return { models, error: null };
}

async function getMistralModels(apiKey = null) {
  const { OpenAI: OpenAIApi } = require("openai");
  const openai = new OpenAIApi({
    apiKey: apiKey || process.env.MISTRAL_API_KEY || null,
    baseURL: "https://api.mistral.ai/v1",
  });
  const models = await openai.models
    .list()
    .then((results) =>
      results.data.filter((model) => !model.id.includes("embed"))
    )
    .catch((e) => {
      console.error(`Mistral:listModels`, e.message);
      return [];
    });

  // Api Key was successful so lets save it for future uses
  if (models.length > 0 && !!apiKey) process.env.MISTRAL_API_KEY = apiKey;
  return { models, error: null };
}

async function getElevenLabsModels(apiKey = null) {
  const models = (await ElevenLabsTTS.voices(apiKey)).map((model) => {
    return {
      id: model.voice_id,
      organization: model.category,
      name: model.name,
    };
  });

  if (models.length === 0) {
    return {
      models: [
        {
          id: "21m00Tcm4TlvDq8ikWAM",
          organization: "premade",
          name: "Rachel (default)",
        },
      ],
      error: null,
    };
  }

  if (models.length > 0 && !!apiKey) process.env.TTS_ELEVEN_LABS_KEY = apiKey;
  return { models, error: null };
}

async function getMinimaxModels(_apiKey = null) {
  const { OpenAI: OpenAIApi } = require("openai");
  const apiKey =
    _apiKey === true
      ? process.env.MINIMAX_API_KEY
      : _apiKey || process.env.MINIMAX_API_KEY || null;
  const openai = new OpenAIApi({
    baseURL: "https://api.minimax.io/v1",
    apiKey,
  });
  const models = await openai.models
    .list()
    .then((results) => results.data)
    .then((models) =>
      models.map((model) => ({
        id: model.id,
        name: model.id,
        organization: model.owned_by || "minimax",
      }))
    )
    .catch((e) => {
      console.error(`Minimax:listModels`, e.message);
      return [
        {
          id: "MiniMax-M2.7",
          name: "MiniMax-M2.7",
          organization: "minimax",
        },
        {
          id: "MiniMax-M2.7-highspeed",
          name: "MiniMax-M2.7-highspeed",
          organization: "minimax",
        },
        {
          id: "MiniMax-M2.5",
          name: "MiniMax-M2.5",
          organization: "minimax",
        },
        {
          id: "MiniMax-M2.5-highspeed",
          name: "MiniMax-M2.5-highspeed",
          organization: "minimax",
        },
        {
          id: "MiniMax-M2.1",
          name: "MiniMax-M2.1",
          organization: "minimax",
        },
        {
          id: "MiniMax-M2.1-highspeed",
          name: "MiniMax-M2.1-highspeed",
          organization: "minimax",
        },
        {
          id: "MiniMax-M2",
          name: "MiniMax-M2",
          organization: "minimax",
        },
      ];
    });

  // Api Key was successful so lets save it for future uses
  if (models.length > 0 && !!apiKey) process.env.MINIMAX_API_KEY = apiKey;
  return { models, error: null };
}

async function getDeepSeekModels(apiKey = null) {
  const { OpenAI: OpenAIApi } = require("openai");
  const openai = new OpenAIApi({
    apiKey: apiKey || process.env.DEEPSEEK_API_KEY,
    baseURL: "https://api.deepseek.com/v1",
  });
  const models = await openai.models
    .list()
    .then((results) => results.data)
    .then((models) =>
      models.map((model) => ({
        id: model.id,
        name: model.id,
        organization: model.owned_by,
      }))
    )
    .catch((e) => {
      console.error(`DeepSeek:listModels`, e.message);
      return [
        {
          id: "deepseek-chat",
          name: "deepseek-chat",
          organization: "deepseek",
        },
        {
          id: "deepseek-reasoner",
          name: "deepseek-reasoner",
          organization: "deepseek",
        },
      ];
    });

  if (models.length > 0 && !!apiKey) process.env.DEEPSEEK_API_KEY = apiKey;
  return { models, error: null };
}

async function getGiteeAIModels() {
  const { giteeAiModels } = require("../AiProviders/giteeai");
  const modelMap = await giteeAiModels();
  if (!Object.keys(modelMap).length === 0) return { models: [], error: null };
  const models = Object.values(modelMap).map((model) => {
    return {
      id: model.id,
      organization: model.organization ?? "GiteeAI",
      name: model.id,
    };
  });
  return { models, error: null };
}

async function getXAIModels(_apiKey = null) {
  const { OpenAI: OpenAIApi } = require("openai");
  const apiKey =
    _apiKey === true
      ? process.env.XAI_LLM_API_KEY
      : _apiKey || process.env.XAI_LLM_API_KEY || null;
  const openai = new OpenAIApi({
    baseURL: "https://api.x.ai/v1",
    apiKey,
  });
  const models = await openai.models
    .list()
    .then((results) => results.data)
    .catch((e) => {
      console.error(`XAI:listModels`, e.message);
      return [
        {
          created: 1725148800,
          id: "grok-beta",
          object: "model",
          owned_by: "xai",
        },
      ];
    });

  // Api Key was successful so lets save it for future uses
  if (models.length > 0 && !!apiKey) process.env.XAI_LLM_API_KEY = apiKey;
  return { models, error: null };
}

async function getNvidiaNimModels(basePath = null) {
  try {
    const { OpenAI: OpenAIApi } = require("openai");
    const openai = new OpenAIApi({
      baseURL: parseNvidiaNimBasePath(
        basePath ?? process.env.NVIDIA_NIM_LLM_BASE_PATH
      ),
      apiKey: null,
    });
    const modelResponse = await openai.models
      .list()
      .then((results) => results.data)
      .catch((e) => {
        throw new Error(e.message);
      });

    const models = modelResponse.map((model) => {
      return {
        id: model.id,
        name: model.id,
        organization: model.owned_by,
      };
    });

    return { models, error: null };
  } catch (e) {
    console.error(`NVIDIA NIM:getNvidiaNimModels`, e.message);
    return { models: [], error: "Could not fetch NVIDIA NIM Models" };
  }
}

async function getGeminiModels(_apiKey = null) {
  const apiKey =
    _apiKey === true
      ? process.env.GEMINI_API_KEY
      : _apiKey || process.env.GEMINI_API_KEY || null;
  const models = await GeminiLLM.fetchModels(apiKey);
  // Api Key was successful so lets save it for future uses
  if (models.length > 0 && !!apiKey) process.env.GEMINI_API_KEY = apiKey;
  return { models, error: null };
}

async function getPPIOModels() {
  const ppioModels = await fetchPPIOModels();
  if (!Object.keys(ppioModels).length === 0) return { models: [], error: null };
  const models = Object.values(ppioModels).map((model) => {
    return {
      id: model.id,
      organization: model.organization,
      name: model.name,
    };
  });
  return { models, error: null };
}

function getNativeEmbedderModels() {
  const { NativeEmbedder } = require("../EmbeddingEngines/native");
  return { models: NativeEmbedder.availableModels(), error: null };
}

async function getMoonshotAiModels(_apiKey = null) {
  const apiKey =
    _apiKey === true
      ? process.env.MOONSHOT_AI_API_KEY
      : _apiKey || process.env.MOONSHOT_AI_API_KEY || null;

  const { OpenAI: OpenAIApi } = require("openai");
  const openai = new OpenAIApi({
    baseURL: "https://api.moonshot.ai/v1",
    apiKey,
  });
  const models = await openai.models
    .list()
    .then((results) => results.data)
    .catch((e) => {
      console.error(`MoonshotAi:listModels`, e.message);
      return [];
    });

  // Api Key was successful so lets save it for future uses
  if (models.length > 0) process.env.MOONSHOT_AI_API_KEY = apiKey;
  return { models, error: null };
}

async function getFoundryModels(basePath = null) {
  try {
    const { OpenAI: OpenAIApi } = require("openai");
    const openai = new OpenAIApi({
      baseURL: parseFoundryBasePath(basePath || process.env.FOUNDRY_BASE_PATH),
      apiKey: null,
    });
    const models = await openai.models
      .list()
      .then((results) =>
        results.data.map((model) => ({
          ...model,
          name: model.id,
        }))
      )
      .catch((e) => {
        console.error(`Foundry:listModels`, e.message);
        return [];
      });

    return { models, error: null };
  } catch (e) {
    console.error(`Foundry:getFoundryModels`, e.message);
    return { models: [], error: "Could not fetch Foundry Models" };
  }
}

/**
 * Get Cohere models
 * @param {string} _apiKey - The API key to use
 * @param {'chat' | 'embed'} type - The type of model to get
 * @returns {Promise<{models: Array<{id: string, organization: string, name: string}>, error: string | null}>}
 */
async function getCohereModels(_apiKey = null, type = "chat") {
  const apiKey =
    _apiKey === true
      ? process.env.COHERE_API_KEY
      : _apiKey || process.env.COHERE_API_KEY || null;

  // Cohere's models endpoint is queried directly so we can keep filtering by
  // endpoint (chat/embed) which the OpenAI-compatible /models route does not support.
  const models = await fetch(
    `https://api.cohere.com/v1/models?page_size=1000&endpoint=${type}`,
    {
      method: "GET",
      headers: { Authorization: `Bearer ${apiKey}` },
    }
  )
    .then((res) => res.json())
    .then((data) => data?.models || [])
    .then((models) =>
      models.map((model) => ({
        id: model.name,
        name: model.name,
      }))
    )
    .catch((e) => {
      console.error(`Cohere:listModels`, e.message);
      return [];
    });

  return { models, error: null };
}

async function getZAiModels(_apiKey = null) {
  const { OpenAI: OpenAIApi } = require("openai");
  const apiKey =
    _apiKey === true
      ? process.env.ZAI_API_KEY
      : _apiKey || process.env.ZAI_API_KEY || null;
  const openai = new OpenAIApi({
    baseURL: "https://api.z.ai/api/paas/v4",
    apiKey,
  });
  const models = await openai.models
    .list()
    .then((results) => results.data)
    .catch((e) => {
      console.error(`Z.AI:listModels`, e.message);
      return [];
    });

  // Api Key was successful so lets save it for future uses
  if (models.length > 0 && !!apiKey) process.env.ZAI_API_KEY = apiKey;
  return { models, error: null };
}

async function getOpenRouterEmbeddingModels() {
  const knownModels = await fetchOpenRouterEmbeddingModels();
  if (!Object.keys(knownModels).length === 0)
    return { models: [], error: null };

  const models = Object.values(knownModels).map((model) => {
    return {
      id: model.id,
      organization: model.organization,
      name: model.name,
    };
  });
  return { models, error: null };
}

async function getDockerModelRunnerModels(basePath = null) {
  try {
    const models = await getDockerModels(basePath);
    return { models, error: null };
  } catch (e) {
    console.error(`DockerModelRunner:getDockerModelRunnerModels`, e.message);
    return {
      models: [],
      error: "Could not fetch Docker Model Runner Models",
    };
  }
}

async function getLemonadeModels(basePath = null, task = "chat") {
  try {
    const models = await getAllLemonadeModels(basePath, task);
    return { models, error: null };
  } catch (e) {
    console.error(`Lemonade:getLemonadeModels`, e.message);
    return { models: [], error: "Could not fetch Lemonade Models" };
  }
}

async function getLemonadeSTTModels(basePath = null) {
  try {
    const models = await getAllLemonadeModels(basePath, "transcription");
    return { models, error: null };
  } catch (e) {
    console.error(`Lemonade:getLemonadeSTTModels`, e.message);
    return { models: [], error: "Could not fetch Lemonade STT Models" };
  }
}

/**
 * Get Deepgram STT models from the Management API.
 * https://api.deepgram.com/v1/models returns { stt: [...], tts: [...] }.
 * @param {string} _apiKey - Deepgram API key. Falls back to STT_DEEPGRAM_API_KEY.
 * @returns {Promise<{models: Array<{id: string, name: string, organization: string}>, error: string | null}>}
 */
async function getDeepgramSTTModels(_apiKey = null) {
  const apiKey =
    _apiKey === true
      ? process.env.STT_DEEPGRAM_API_KEY
      : _apiKey || process.env.STT_DEEPGRAM_API_KEY || null;
  if (!apiKey)
    return { models: [], error: "No Deepgram API key was provided." };

  try {
    const response = await fetch("https://api.deepgram.com/v1/models", {
      method: "GET",
      headers: { Authorization: `Token ${apiKey}` },
    });
    if (!response.ok) throw new Error(`Deepgram returned ${response.status}`);

    let models = new Map();
    const data = await response.json();
    (data?.stt ?? [])
      .filter((m) => m.batch !== false)
      .forEach((m) => {
        if (models.has(m.canonical_name)) return;
        models.set(m.canonical_name, {
          id: m.canonical_name,
          name: m.canonical_name,
          organization: "Deepgram",
        });
      });

    models = Array.from(models.values());
    // Api Key was successful so lets save it for future uses
    if (models.length > 0 && _apiKey) process.env.STT_DEEPGRAM_API_KEY = apiKey;
    return { models, error: null };
  } catch (e) {
    console.error(`Deepgram:getDeepgramSTTModels`, e.message);
    return { models: [], error: "Could not fetch Deepgram STT models" };
  }
}

/**
 * Get Privatemode models
 * @param {string} basePath - The base path of the Privatemode endpoint.
 * @param {'any' | 'generate' | 'embed' | 'transcribe'} task - The task to fetch the models for.
 * @returns {Promise<{models: Array<{id: string, organization: string, name: string}>, error: string | null}>}
 */
async function getPrivatemodeModels(basePath = null, task = "any") {
  try {
    const { PrivatemodeLLM } = require("../AiProviders/privatemode");
    const { OpenAI: OpenAIApi } = require("openai");
    const openai = new OpenAIApi({
      baseURL: PrivatemodeLLM.parseBasePath(
        basePath || process.env.PRIVATEMODE_LLM_BASE_PATH
      ),
      apiKey: null,
    });
    const models = await openai.models
      .list()
      .then((results) => results.data)
      .then(
        (models) =>
          models
            .filter((model) => !model.id.includes("/")) // remove legacy prefixed models
            .filter((model) =>
              task === "any" ? true : model.tasks.includes(task)
            ) // filter by task or show all if task is any
      )
      .then((models) =>
        models.map((model) => ({
          id: model.id,
          organization: "Privatemode",
          name: model.id
            .split("-")
            .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
            .join(" "),
        }))
      )
      .catch((e) => {
        console.error(`Privatemode:listModels`, e.message);
        return [];
      });
    return { models, error: null };
  } catch (e) {
    console.error(`Privatemode:getPrivatemodeModels`, e.message);
    return { models: [], error: "Could not fetch Privatemode Models" };
  }
}

/**
 * Get SambaNova models
 * @param {string} _apiKey - The API key to use
 * @returns {Promise<{models: Array<{id: string, organization: string, name: string}>, error: string | null}>}
 */
async function getSambaNovaModels(_apiKey = null) {
  try {
    const apiKey =
      _apiKey === true
        ? process.env.SAMBANOVA_LLM_API_KEY
        : _apiKey || process.env.SAMBANOVA_LLM_API_KEY || null;
    const { OpenAI: OpenAIApi } = require("openai");
    const openai = new OpenAIApi({
      baseURL: "https://api.sambanova.ai/v1",
      apiKey,
    });
    const models = await openai.models
      .list()
      .then((results) => results.data)
      .then((models) =>
        models.filter((model) => !model.id.toLowerCase().startsWith("whisper"))
      )
      .then((models) =>
        models.map((model) => {
          const organization =
            model.hasOwnProperty("owned_by") &&
            model.owned_by !== "no-reply@sambanova.ai"
              ? model.owned_by
              : "SambaNova";
          return {
            id: model.id,
            organization,
            name: model.id,
          };
        })
      )
      .catch((e) => {
        console.error(`SambaNova:listModels`, e.message);
        return [];
      });
    return { models, error: null };
  } catch (e) {
    console.error(`SambaNova:getSambaNovaModels`, e.message);
    return { models: [], error: "Could not fetch SambaNova Models" };
  }
}

/**
 * Use the Cerebras PUBLIC API to fetch the public models
 * @returns {Promise<{models: Array<{id: string, organization: string, name: string}>, error: string | null}>}
 */
async function getCerebrasModels() {
  try {
    const models = await fetch("https://api.cerebras.ai/public/v1/models")
      .then((response) => response.json())
      .then(({ data = [] }) => {
        return data.map((model) => ({
          id: model.id,
          name: model.name,
          organization: model.owned_by ?? "Cerebras",
        }));
      })
      .catch((error) => {
        console.error(`Cerebras:listModels`, error.message);
        return [];
      });
    return { models, error: null };
  } catch (e) {
    console.error(`Cerebras:getCerebrasModels`, e.message);
    return { models: [], error: "Could not fetch Cerebras Models" };
  }
}

async function getGenericOpenAiModels(basePath = null, apiKey = null) {
  try {
    const { OpenAI: OpenAIApi } = require("openai");
    const openai = new OpenAIApi({
      baseURL: basePath || process.env.GENERIC_OPEN_AI_BASE_PATH,
      apiKey: apiKey || process.env.GENERIC_OPEN_AI_API_KEY || null,
    });
    const models = await openai.models
      .list()
      .then((results) => results.data)
      .then((models) =>
        models.map((model) => ({
          id: model.id,
          name: model.id,
          organization: model.owned_by ?? "generic-openai",
        }))
      )
      .catch((e) => {
        console.error(`GenericOpenAI:listModels`, e.message);
        return [];
      });

    if (models.length > 0 && !!apiKey)
      process.env.GENERIC_OPEN_AI_API_KEY = apiKey;
    return { models, error: null };
  } catch (e) {
    console.error(`GenericOpenAI:getGenericOpenAiModels`, e.message);
    return { models: [], error: "Could not fetch Generic OpenAI Models" };
  }
}

/**
 * Pulls the live voice list from a self-hosted kokoro-fastapi server's
 * /audio/voices endpoint. basePath is the OpenAI-compatible base URL the
 * user pointed at their kokoro instance (e.g. http://localhost:8880/v1).
 * @param {string} basePath - The base path to the Kokoro instance.
 * @param {string} apiKey - The API key to use.
 * @returns {Promise<{models: Array<{id: string, organization: string, name: string}>, error: string | null}>}
 */
async function kokoroTtsVoices(basePath = null, apiKey = null) {
  let endpoint = basePath || process.env.TTS_KOKORO_ENDPOINT;
  if (!endpoint)
    return { models: [], error: "No Kokoro endpoint was provided." };

  endpoint = new URL(endpoint);
  endpoint.pathname = "/v1/audio/voices";
  const headers = { "Content-Type": "application/json" };
  const key = typeof apiKey === "boolean" ? null : apiKey;
  if (key) headers.Authorization = `Bearer ${key}`;

  const voices = await fetch(endpoint.toString(), { method: "GET", headers })
    .then((res) => {
      if (!res.ok) throw new Error(res.statusText || "Failed to load voices");
      return res.json();
    })
    .then((data) => (Array.isArray(data?.voices) ? data.voices : []))
    .catch((e) => {
      console.error(`Kokoro:listVoices`, e.message);
      return null;
    });

  if (!voices || !Array.isArray(voices))
    return { models: [], error: "Could not fetch Kokoro voices." };
  const models = voices.map((voice) => ({
    id: voice.id,
    name: voice.name,
    organization: "Kokoro",
  }));
  return { models, error: null };
}

/**
 * Get AWS Bedrock models
 * @param {string} _apiKey - The API key to use
 * @param {Object} options - The options to use
 * @param {string} [options.region] - The region to use
 * @returns {Promise<{models: Array<{id: string, organization: string, name: string}>, error: string | null}>}
 */
async function getBedrockModels(_apiKey = null, options = {}) {
  try {
    const apiKey =
      _apiKey === true
        ? process.env.AWS_BEDROCK_LLM_API_KEY
        : _apiKey || process.env.AWS_BEDROCK_LLM_API_KEY || null;
    const region =
      options?.region || process.env.AWS_BEDROCK_LLM_REGION || "us-west-2";

    const { OpenAI: OpenAIApi } = require("openai");
    const openai = new OpenAIApi({
      apiKey,
      baseURL: `https://bedrock-mantle.${region}.api.aws/v1`,
    });
    const models = await openai.models
      .list()
      .then((results) => results.data)
      .then((models) =>
        models.map((model) => ({
          id: model.id,
          name: model.id,
          organization: model.owned_by ?? "AWS Bedrock",
        }))
      )
      .catch((e) => {
        console.error(`AWSBedrock:listModels`, e.message);
        return [];
      });

    if (models.length > 0 && !!apiKey)
      process.env.AWS_BEDROCK_LLM_API_KEY = apiKey;
    return { models, error: null };
  } catch (e) {
    console.error(`AWSBedrock:getBedrockModels`, e.message);
    return { models: [], error: "Could not fetch AWS Bedrock Models" };
  }
}

module.exports = {
  getCustomModels,
  SUPPORT_CUSTOM_MODELS,
};
