Greasy Fork is available in English.

SmolLLM

LLM utility library

Verze ze dne 04. 03. 2025. Zobrazit nejnovější verzi.

Tento skript by neměl být instalován přímo. Jedná se o knihovnu, kterou by měly jiné skripty využívat pomocí meta příkazu // @require https://update.greatest.deepsurf.us/scripts/528704/1546694/SmolLLM.js

  1. // ==UserScript==
  2. // @name SmolLLM
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.1.6
  5. // @description LLM utility library
  6. // @author RoCry
  7. // @grant GM_xmlhttpRequest
  8. // @require https://update.greatest.deepsurf.us/scripts/528703/1546610/SimpleBalancer.js
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. class SmolLLM {
  13. constructor() {
  14. // Ensure SimpleBalancer is available
  15. if (typeof SimpleBalancer === 'undefined') {
  16. throw new Error('SimpleBalancer is required for SmolLLM to work');
  17. }
  18.  
  19. // Verify GM_xmlhttpRequest is available
  20. if (typeof GM_xmlhttpRequest === 'undefined') {
  21. throw new Error('GM_xmlhttpRequest is required for SmolLLM to work');
  22. }
  23.  
  24. this.balancer = new SimpleBalancer();
  25. this.logger = console;
  26. }
  27.  
  28. /**
  29. * Prepares request data based on the provider
  30. *
  31. * @param {string} prompt - User prompt
  32. * @param {string} systemPrompt - System prompt
  33. * @param {string} modelName - Model name
  34. * @param {string} providerName - Provider name (anthropic, openai, gemini)
  35. * @param {string} baseUrl - API base URL
  36. * @returns {Object} - {url, data} for the request
  37. */
  38. prepareRequestData(prompt, systemPrompt, modelName, providerName, baseUrl) {
  39. baseUrl = baseUrl.trim().replace(/\/+$/, '');
  40.  
  41. let url, data;
  42.  
  43. if (providerName === 'anthropic') {
  44. url = `${baseUrl}/v1/messages`;
  45. data = {
  46. model: modelName,
  47. max_tokens: 4096,
  48. messages: [{ role: 'user', content: prompt }],
  49. stream: true
  50. };
  51. if (systemPrompt) {
  52. data.system = systemPrompt;
  53. }
  54. } else if (providerName === 'gemini') {
  55. url = `${baseUrl}/v1beta/models/${modelName}:streamGenerateContent?alt=sse`;
  56. data = {
  57. contents: [{ parts: [{ text: prompt }] }]
  58. };
  59. if (systemPrompt) {
  60. data.system_instruction = { parts: [{ text: systemPrompt }] };
  61. }
  62. } else {
  63. // OpenAI compatible APIs
  64. const messages = [];
  65. if (systemPrompt) {
  66. messages.push({ role: 'system', content: systemPrompt });
  67. }
  68. messages.push({ role: 'user', content: prompt });
  69.  
  70. data = {
  71. messages: messages,
  72. model: modelName,
  73. stream: true
  74. };
  75.  
  76. // Handle URL based on suffix
  77. if (baseUrl.endsWith('#')) {
  78. url = baseUrl.slice(0, -1); // Remove the # and use exact URL
  79. } else if (baseUrl.endsWith('/')) {
  80. url = `${baseUrl}chat/completions`; // Skip v1 prefix
  81. } else {
  82. url = `${baseUrl}/v1/chat/completions`; // Default pattern
  83. }
  84. }
  85.  
  86. return { url, data };
  87. }
  88.  
  89. /**
  90. * Prepares headers for authentication based on the provider
  91. *
  92. * @param {string} providerName - Provider name
  93. * @param {string} apiKey - API key
  94. * @returns {Object} - Request headers
  95. */
  96. prepareHeaders(providerName, apiKey) {
  97. const headers = {
  98. 'Content-Type': 'application/json'
  99. };
  100.  
  101. if (providerName === 'anthropic') {
  102. headers['X-API-Key'] = apiKey;
  103. headers['Anthropic-Version'] = '2023-06-01';
  104. } else if (providerName === 'gemini') {
  105. headers['X-Goog-Api-Key'] = apiKey;
  106. } else {
  107. headers['Authorization'] = `Bearer ${apiKey}`;
  108. }
  109.  
  110. return headers;
  111. }
  112.  
  113. /**
  114. * Process SSE stream data for different providers
  115. *
  116. * @param {string} chunk - Data chunk from SSE
  117. * @param {string} providerName - Provider name
  118. * @returns {string|null} - Extracted text content or null
  119. */
  120. processStreamChunk(chunk, providerName) {
  121. if (!chunk || chunk === '[DONE]') return null;
  122.  
  123. try {
  124. console.log(`Processing chunk for ${providerName}:`, chunk.substring(0, 100) + (chunk.length > 100 ? '...' : ''));
  125. const data = JSON.parse(chunk);
  126.  
  127. // Follow the Python implementation pattern for cleaner provider-specific handling
  128. if (providerName === 'gemini') {
  129. const candidates = data.candidates || [];
  130. if (candidates.length > 0 && candidates[0].content) {
  131. if (candidates[0].content.parts && candidates[0].content.parts.length > 0) {
  132. return candidates[0].content.parts[0].text || '';
  133. }
  134. }
  135. } else if (providerName === 'anthropic') {
  136. // Handle content_block_delta which contains the actual text
  137. if (data.type === 'content_block_delta') {
  138. const delta = data.delta || {};
  139. if (delta.type === 'text_delta' || delta.text) {
  140. return delta.text || '';
  141. }
  142. }
  143. // Anthropic sends various event types - only some contain text
  144. return null;
  145. } else {
  146. // OpenAI compatible format
  147. const choice = (data.choices || [{}])[0];
  148. if (choice.finish_reason !== null && choice.finish_reason !== undefined) {
  149. return null; // End of generation
  150. }
  151. return choice.delta && choice.delta.content ? choice.delta.content : null;
  152. }
  153. } catch (e) {
  154. console.error(`Error parsing chunk: ${e.message}, chunk: ${chunk}`);
  155. return null;
  156. }
  157.  
  158. return null;
  159. }
  160.  
  161. /**
  162. * Makes a request to the LLM API and handles streaming responses
  163. *
  164. * @param {Object} params - Request parameters
  165. * @returns {Promise<string>} - Full response text
  166. */
  167. async askLLM({
  168. prompt,
  169. providerName,
  170. systemPrompt = '',
  171. model,
  172. apiKey,
  173. baseUrl,
  174. handler = null,
  175. timeout = 60000
  176. }) {
  177. if (!prompt || !providerName || !model || !apiKey || !baseUrl) {
  178. throw new Error('Required parameters missing');
  179. }
  180.  
  181. // Use balancer to choose API key and base URL pair
  182. [apiKey, baseUrl] = this.balancer.choosePair(apiKey, baseUrl);
  183.  
  184. const { url, data } = this.prepareRequestData(
  185. prompt, systemPrompt, model, providerName, baseUrl
  186. );
  187.  
  188. const headers = this.prepareHeaders(providerName, apiKey);
  189.  
  190. // Log request info (with masked API key)
  191. const apiKeyPreview = `${apiKey.slice(0, 5)}...${apiKey.slice(-4)}`;
  192. this.logger.info(
  193. `Sending ${url} model=${model} api_key=${apiKeyPreview}, len=${prompt.length}`
  194. );
  195.  
  196. return new Promise((resolve, reject) => {
  197. let responseText = '';
  198. let buffer = '';
  199. let timeoutId;
  200.  
  201. // Set timeout
  202. if (timeout) {
  203. timeoutId = setTimeout(() => {
  204. reject(new Error(`Request timed out after ${timeout}ms`));
  205. }, timeout);
  206. }
  207.  
  208. GM_xmlhttpRequest({
  209. method: 'POST',
  210. url: url,
  211. headers: headers,
  212. data: JSON.stringify(data),
  213. responseType: 'stream',
  214. onload: (response) => {
  215. // This won't be called for streaming responses
  216. if (response.status !== 200) {
  217. clearTimeout(timeoutId);
  218. reject(new Error(`API request failed: ${response.status} - ${response.responseText}`));
  219. }
  220. },
  221. onreadystatechange: (state) => {
  222. if (state.readyState === 4) {
  223. // Request completed
  224. clearTimeout(timeoutId);
  225. console.log(`Request completed with response text length: ${responseText.length}`);
  226. resolve(responseText);
  227. }
  228. },
  229. onprogress: (response) => {
  230. // Handle streaming response
  231. const chunk = response.responseText.substring(buffer.length);
  232. buffer = response.responseText;
  233.  
  234. console.log(`Received chunk size: ${chunk.length}`);
  235.  
  236. if (!chunk) return;
  237.  
  238. // Process SSE format (data: {...}\n\n) - following Python implementation pattern
  239. const lines = chunk.split('\n');
  240.  
  241. for (const line of lines) {
  242. const trimmed = line.trim();
  243. if (!trimmed || trimmed === 'data: [DONE]' || !trimmed.startsWith('data: ')) continue;
  244.  
  245. try {
  246. // Remove 'data: ' prefix (6 characters)
  247. const content = trimmed.substring(6);
  248. const textChunk = this.processStreamChunk(content, providerName);
  249.  
  250. if (textChunk) {
  251. responseText += textChunk;
  252. if (handler && typeof handler === 'function') {
  253. handler(textChunk);
  254. }
  255. }
  256. } catch (e) {
  257. console.error('Error processing line:', e, trimmed);
  258. }
  259. }
  260. },
  261. onerror: (error) => {
  262. clearTimeout(timeoutId);
  263. console.error('Request error:', error);
  264. reject(new Error(`Request failed: ${error.error || JSON.stringify(error)}`));
  265. },
  266. ontimeout: () => {
  267. clearTimeout(timeoutId);
  268. reject(new Error(`Request timed out after ${timeout}ms`));
  269. }
  270. });
  271. });
  272. }
  273. }
  274.  
  275. // Make it available globally
  276. window.SmolLLM = SmolLLM;
  277.  
  278. // Export for module systems if needed
  279. if (typeof module !== 'undefined') {
  280. module.exports = SmolLLM;
  281. }