SmolLLM

LLM utility library

Dette script bør ikke installeres direkte. Det er et bibliotek, som andre scripts kan inkludere med metadirektivet // @require https://update.greatest.deepsurf.us/scripts/528704/1732957/SmolLLM.js

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         SmolLLM
// @namespace    http://tampermonkey.net/
// @version      0.2.0
// @description  LLM utility library
// @author       RoCry
// @require https://update.greatest.deepsurf.us/scripts/528703/1732956/SimpleBalancer.js
// @license MIT
// ==/UserScript==

class SmolLLM {
  constructor() {
    if (typeof SimpleBalancer === "undefined") {
      throw new Error("SimpleBalancer is required for SmolLLM to work");
    }

    this.balancer = new SimpleBalancer();
    this.logger = console;
    this.buffer = "";
  }

  prepareRequestData(prompt, systemPrompt, modelName, providerName, baseUrl) {
    const provider = providerName.toLowerCase();
    let url;
    let data;

    if (provider === "anthropic") {
      url = `${baseUrl}/v1/messages`;
      data = {
        model: modelName,
        max_tokens: 4096,
        messages: [{ role: "user", content: prompt }],
        stream: true,
      };
      if (systemPrompt) {
        data.system = systemPrompt;
      }
      return { url, data };
    }

    if (provider === "gemini") {
      url = `${baseUrl}/v1beta/models/${modelName}:streamGenerateContent?alt=sse`;
      data = {
        contents: [{ parts: [{ text: prompt }] }],
      };
      if (systemPrompt) {
        data.system_instruction = { parts: [{ text: systemPrompt }] };
      }
      return { url, data };
    }

    const messages = [];
    if (systemPrompt) {
      messages.push({ role: "system", content: systemPrompt });
    }
    messages.push({ role: "user", content: prompt });

    data = {
      messages,
      model: modelName,
      stream: true,
    };

    if (baseUrl.endsWith("#")) {
      url = baseUrl.slice(0, -1);
    } else if (baseUrl.endsWith("/")) {
      url = `${baseUrl}chat/completions`;
    } else {
      url = `${baseUrl}/v1/chat/completions`;
    }

    return { url, data };
  }

  prepareHeaders(providerName, apiKey) {
    const provider = providerName.toLowerCase();
    const headers = {
      "Content-Type": "application/json",
    };

    if (provider === "anthropic") {
      headers["X-API-Key"] = apiKey;
      headers["Anthropic-Version"] = "2023-06-01";
      return headers;
    }

    if (provider === "gemini") {
      headers["X-Goog-Api-Key"] = apiKey;
      return headers;
    }

    headers["Authorization"] = `Bearer ${apiKey}`;
    return headers;
  }

  extractTextFromChunk(data, providerName) {
    try {
      const provider = providerName.toLowerCase();

      if (provider === "gemini") {
        const candidates = data.candidates || [];
        if (candidates.length > 0 && candidates[0].content) {
          const parts = candidates[0].content.parts;
          if (parts && parts.length > 0) {
            return parts[0].text || "";
          }
        }
        return null;
      }

      if (provider === "anthropic") {
        if (data.type === "content_block_delta") {
          const delta = data.delta || {};
          if (delta.type === "text_delta" || delta.text) {
            return delta.text || "";
          }
        }
        return null;
      }

      const choices = data.choices || [];
      if (choices.length === 0) return null;

      const choice = choices[0];
      if (choice.finish_reason) return null;

      if (choice.delta && choice.delta.content) {
        return choice.delta.content;
      }

      return null;
    } catch (error) {
      this.logger.error(`Error extracting text from chunk: ${error.message}`);
      return null;
    }
  }

  async askLLM({
    prompt,
    providerName,
    systemPrompt = "",
    model,
    apiKey,
    baseUrl,
    handler = null,
    timeout = 60000,
  }) {
    if (!prompt || !providerName || !model || !apiKey || !baseUrl) {
      throw new Error("Required parameters missing");
    }

    const normalizedProvider = providerName.toLowerCase();
    const trimmedKey = String(apiKey).trim();
    const trimmedBaseUrl = String(baseUrl).trim();

    let selectedKey;
    let selectedUrl;

    [selectedKey, selectedUrl] = this.balancer.choosePair(
      trimmedKey,
      trimmedBaseUrl,
    );

    const { url, data } = this.prepareRequestData(
      prompt,
      systemPrompt,
      model,
      normalizedProvider,
      selectedUrl,
    );

    const headers = this.prepareHeaders(normalizedProvider, selectedKey);
    const apiKeyPreview = `${selectedKey.slice(0, 5)}...${selectedKey.slice(-4)}`;

    this.logger.info(
      `[SmolLLM] Request: ${url} | model=${model} | provider=${normalizedProvider} | api_key=${apiKeyPreview} | prompt=${prompt.length}`,
    );

    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);

    try {
      const response = await fetch(url, {
        method: "POST",
        headers,
        body: JSON.stringify(data),
        signal: controller.signal,
      });

      if (!response.ok) {
        const bodyText = await response.text();
        throw new Error(
          `HTTP error ${response.status}: ${bodyText || "Unknown error"}`,
        );
      }

      if (!response.body) {
        throw new Error("Response body is empty");
      }

      this.buffer = "";

      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let fullText = "";

      while (true) {
        const { done, value } = await reader.read();
        if (done) break;

        const chunk = decoder.decode(value, { stream: true });
        const deltas = this.processStreamChunks(chunk, normalizedProvider);

        for (const delta of deltas) {
          if (!delta) continue;
          fullText += delta;
          if (handler) handler(delta, fullText);
        }
      }

      if (this.buffer) {
        const deltas = this.processStreamChunks("\n", normalizedProvider);
        for (const delta of deltas) {
          if (!delta) continue;
          fullText += delta;
          if (handler) handler(delta, fullText);
        }
      }

      return fullText;
    } catch (error) {
      if (error.name === "AbortError") {
        throw new Error(`Request timed out after ${timeout}ms`);
      }
      throw error;
    } finally {
      clearTimeout(timeoutId);
    }
  }

  processStreamChunks(chunk, providerName) {
    const deltas = [];
    const normalized = chunk.replace(/\r/g, "");

    this.buffer += normalized;

    const lines = this.buffer.split("\n");
    this.buffer = lines.pop() || "";

    for (const line of lines) {
      const trimmed = line.trim();
      if (!trimmed) continue;
      if (!trimmed.startsWith("data: ")) continue;

      const data = trimmed.slice(6).trim();
      if (data === "[DONE]") continue;

      try {
        const jsonData = JSON.parse(data);
        const delta = this.extractTextFromChunk(jsonData, providerName);
        if (delta) deltas.push(delta);
      } catch (error) {
        if (error instanceof SyntaxError) {
          this.logger.log(`Incomplete or invalid JSON: ${data}`);
        } else {
          this.logger.error(
            `Error processing chunk: ${error.message}, chunk: ${data}`,
          );
        }
      }
    }

    return deltas;
  }
}

window.SmolLLM = SmolLLM;

if (typeof module !== "undefined") {
  module.exports = SmolLLM;
}