Greasy Fork 还支持 简体中文。

SmolLLM

LLM utility library

Fra 04.03.2025. Se den seneste versjonen.

Dette scriptet burde ikke installeres direkte. Det er et bibliotek for andre script å inkludere med det nye metadirektivet // @require https://updategf.qytechs.cn/scripts/528704/1546611/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.1
// @description  LLM utility library
// @author       RoCry
// @grant        none
// @require https://update.greasyfork.org/scripts/528703/1546610/SimpleBalancer.js
// @license MIT
// ==/UserScript==

class SmolLLM {
  constructor() {
    // Ensure SimpleBalancer is available
    if (typeof SimpleBalancer === 'undefined') {
      throw new Error('SimpleBalancer is required for SmolLLM to work');
    }
    this.balancer = new SimpleBalancer();
    this.logger = console;
  }

  /**
   * Prepares request data based on the provider
   * 
   * @param {string} prompt - User prompt
   * @param {string} systemPrompt - System prompt 
   * @param {string} modelName - Model name
   * @param {string} providerName - Provider name (anthropic, openai, gemini)
   * @param {string} baseUrl - API base URL
   * @returns {Object} - {url, data} for the request
   */
  prepareRequestData(prompt, systemPrompt, modelName, providerName, baseUrl) {
    baseUrl = baseUrl.trim().replace(/\/+$/, '');

    let url, data;

    if (providerName === 'anthropic') {
      url = `${baseUrl}/v1/messages`;
      data = {
        model: modelName,
        max_tokens: 4096,
        messages: [{ role: 'user', content: prompt }],
        stream: true
      };
      if (systemPrompt) {
        data.system = systemPrompt;
      }
    } else if (providerName === 'gemini') {
      url = `${baseUrl}/v1beta/models/${modelName}:streamGenerateContent?alt=sse`;
      data = {
        contents: [{ parts: [{ text: prompt }] }]
      };
      if (systemPrompt) {
        data.system_instruction = { parts: [{ text: systemPrompt }] };
      }
    } else {
      // OpenAI compatible APIs
      const messages = [];
      if (systemPrompt) {
        messages.push({ role: 'system', content: systemPrompt });
      }
      messages.push({ role: 'user', content: prompt });

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

      // Handle URL based on suffix
      if (baseUrl.endsWith('#')) {
        url = baseUrl.slice(0, -1); // Remove the # and use exact URL
      } else if (baseUrl.endsWith('/')) {
        url = `${baseUrl}chat/completions`; // Skip v1 prefix
      } else {
        url = `${baseUrl}/v1/chat/completions`; // Default pattern
      }
    }

    return { url, data };
  }

  /**
   * Prepares headers for authentication based on the provider
   * 
   * @param {string} providerName - Provider name
   * @param {string} apiKey - API key
   * @returns {Object} - Request headers
   */
  prepareHeaders(providerName, apiKey) {
    const headers = {
      'Content-Type': 'application/json'
    };

    if (providerName === 'anthropic') {
      headers['X-API-Key'] = apiKey;
      headers['Anthropic-Version'] = '2023-06-01';
    } else if (providerName === 'gemini') {
      headers['X-Goog-Api-Key'] = apiKey;
    } else {
      headers['Authorization'] = `Bearer ${apiKey}`;
    }

    return headers;
  }

  /**
   * Process SSE stream data for different providers
   * 
   * @param {string} chunk - Data chunk from SSE
   * @param {string} providerName - Provider name
   * @returns {string|null} - Extracted text content or null
   */
  processStreamChunk(chunk, providerName) {
    if (!chunk || chunk === '[DONE]') return null;

    try {
      const data = JSON.parse(chunk);

      if (providerName === 'anthropic') {
        if (data.type === 'content_block_delta' && data.delta && data.delta.text) {
          return data.delta.text;
        }
      } else if (providerName === 'gemini') {
        if (data.candidates &&
          data.candidates[0].content &&
          data.candidates[0].content.parts &&
          data.candidates[0].content.parts[0].text) {
          return data.candidates[0].content.parts[0].text;
        }
      } else {
        // OpenAI compatible
        if (data.choices &&
          data.choices[0].delta &&
          data.choices[0].delta.content) {
          return data.choices[0].delta.content;
        }
      }
    } catch (e) {
      // Ignore parsing errors in stream chunks
      return null;
    }

    return null;
  }

  /**
   * Makes a request to the LLM API and handles streaming responses
   * 
   * @param {Object} params - Request parameters
   * @returns {Promise<string>} - Full response text
   */
  async askLLM({
    prompt,
    providerName,
    systemPrompt = '',
    model,
    apiKey,
    baseUrl,
    handler = null,
    timeout = 60000
  }) {
    if (!prompt || !providerName || !model || !apiKey || !baseUrl) {
      throw new Error('Required parameters missing');
    }

    // Use balancer to choose API key and base URL pair
    [apiKey, baseUrl] = this.balancer.choosePair(apiKey, baseUrl);

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

    const headers = this.prepareHeaders(providerName, apiKey);

    // Log request info (with masked API key)
    const apiKeyPreview = `${apiKey.slice(0, 5)}...${apiKey.slice(-4)}`;
    this.logger.info(
      `Sending ${url} model=${model} api_key=${apiKeyPreview}, len=${prompt.length}`
    );

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

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

      if (!response.ok) {
        const errorText = await response.text();
        throw new Error(`API request failed: ${response.status} - ${errorText}`);
      }

      // Handle streaming response
      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let responseText = '';
      let buffer = '';

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

        const chunk = decoder.decode(value);
        buffer += chunk;

        // Process SSE format (data: {...}\n\n)
        const lines = buffer.split('\n\n');
        buffer = lines.pop() || '';

        for (const line of lines) {
          if (!line.trim() || !line.startsWith('data: ')) continue;

          const content = line.slice(6); // Remove 'data: ' prefix
          const textChunk = this.processStreamChunk(content, providerName);

          if (textChunk) {
            responseText += textChunk;
            if (handler && typeof handler === 'function') {
              handler(textChunk);
            }
          }
        }
      }

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

// Make it available globally
window.SmolLLM = SmolLLM;

// Export for module systems if needed
if (typeof module !== 'undefined') {
  module.exports = SmolLLM;
}