SmolLLM

LLM utility library

Script này sẽ không được không được cài đặt trực tiếp. Nó là một thư viện cho các script khác để bao gồm các chỉ thị meta // @require https://update.greatest.deepsurf.us/scripts/528704/1549030/SmolLLM.js

  1. // ==UserScript==
  2. // @name SmolLLM
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.1.15
  5. // @description LLM utility library
  6. // @author RoCry
  7. // @require https://update.greatest.deepsurf.us/scripts/528703/1546610/SimpleBalancer.js
  8. // @license MIT
  9. // ==/UserScript==
  10.  
  11. class SmolLLM {
  12. constructor() {
  13. if (typeof SimpleBalancer === 'undefined') {
  14. throw new Error('SimpleBalancer is required for SmolLLM to work');
  15. }
  16.  
  17. this.balancer = new SimpleBalancer();
  18. this.logger = console;
  19. this.buffer = ''; // Buffer for incomplete SSE messages
  20. }
  21.  
  22. /**
  23. * Prepares request data based on the provider
  24. *
  25. * @param {string} prompt - User prompt
  26. * @param {string} systemPrompt - System prompt
  27. * @param {string} modelName - Model name
  28. * @param {string} providerName - Provider name (anthropic, openai, gemini)
  29. * @param {string} baseUrl - API base URL
  30. * @returns {Object} - {url, data} for the request
  31. */
  32. prepareRequestData(prompt, systemPrompt, modelName, providerName, baseUrl) {
  33. let url, data;
  34.  
  35. if (providerName === 'anthropic') {
  36. url = `${baseUrl}/v1/messages`;
  37. data = {
  38. model: modelName,
  39. max_tokens: 4096,
  40. messages: [{ role: 'user', content: prompt }],
  41. stream: true
  42. };
  43. if (systemPrompt) {
  44. data.system = systemPrompt;
  45. }
  46. } else if (providerName === 'gemini') {
  47. url = `${baseUrl}/v1beta/models/${modelName}:streamGenerateContent?alt=sse`;
  48. data = {
  49. contents: [{ parts: [{ text: prompt }] }]
  50. };
  51. if (systemPrompt) {
  52. data.system_instruction = { parts: [{ text: systemPrompt }] };
  53. }
  54. } else {
  55. // OpenAI compatible APIs
  56. const messages = [];
  57. if (systemPrompt) {
  58. messages.push({ role: 'system', content: systemPrompt });
  59. }
  60. messages.push({ role: 'user', content: prompt });
  61.  
  62. data = {
  63. messages: messages,
  64. model: modelName,
  65. stream: true
  66. };
  67.  
  68. // Handle URL based on suffix
  69. if (baseUrl.endsWith('#')) {
  70. url = baseUrl.slice(0, -1); // Remove the # and use exact URL
  71. } else if (baseUrl.endsWith('/')) {
  72. url = `${baseUrl}chat/completions`; // Skip v1 prefix
  73. } else {
  74. url = `${baseUrl}/v1/chat/completions`; // Default pattern
  75. }
  76. }
  77.  
  78. return { url, data };
  79. }
  80.  
  81. prepareHeaders(providerName, apiKey) {
  82. const headers = {
  83. 'Content-Type': 'application/json'
  84. };
  85.  
  86. if (providerName === 'anthropic') {
  87. headers['X-API-Key'] = apiKey;
  88. headers['Anthropic-Version'] = '2023-06-01';
  89. } else if (providerName === 'gemini') {
  90. headers['X-Goog-Api-Key'] = apiKey;
  91. } else {
  92. headers['Authorization'] = `Bearer ${apiKey}`;
  93. }
  94.  
  95. return headers;
  96. }
  97.  
  98. /**
  99. * Extract text content from a parsed JSON chunk
  100. *
  101. * @param {Object} data - Parsed JSON data
  102. * @param {string} providerName - Provider name
  103. * @returns {string|null} - Extracted text content or null
  104. */
  105. extractTextFromChunk(data, providerName) {
  106. try {
  107. if (providerName === 'gemini') {
  108. const candidates = data.candidates || [];
  109. if (candidates.length > 0 && candidates[0].content) {
  110. const parts = candidates[0].content.parts;
  111. if (parts && parts.length > 0) {
  112. return parts[0].text || '';
  113. }
  114. }
  115. return null;
  116. }
  117. if (providerName === 'anthropic') {
  118. // Handle content_block_delta which contains the actual text
  119. if (data.type === 'content_block_delta') {
  120. const delta = data.delta || {};
  121. if (delta.type === 'text_delta' || delta.text) {
  122. return delta.text || '';
  123. }
  124. }
  125. return null;
  126. }
  127. // OpenAI compatible format
  128. const choices = data.choices || [];
  129. // Skip if no choices or has filter results only
  130. if (choices.length === 0) {
  131. return null;
  132. }
  133. const choice = choices[0];
  134. // Check if this indicates end of stream
  135. if (choice.finish_reason) {
  136. return null;
  137. }
  138. // Extract content from delta
  139. if (choice.delta && choice.delta.content) {
  140. return choice.delta.content;
  141. }
  142. return null;
  143. } catch (e) {
  144. this.logger.error(`Error extracting text from chunk: ${e.message}`);
  145. return null;
  146. }
  147. }
  148.  
  149. /**
  150. * @returns {Promise<string>} - Full final response text
  151. */
  152. async askLLM({
  153. prompt,
  154. providerName,
  155. systemPrompt = '',
  156. model,
  157. apiKey,
  158. baseUrl,
  159. handler = null, // handler(delta, fullText)
  160. timeout = 60000
  161. }) {
  162. if (!prompt || !providerName || !model || !apiKey || !baseUrl) {
  163. throw new Error('Required parameters missing');
  164. }
  165.  
  166. // Use balancer to choose API key and base URL pair
  167. [apiKey, baseUrl] = this.balancer.choosePair(apiKey, baseUrl);
  168.  
  169. const { url, data } = this.prepareRequestData(
  170. prompt, systemPrompt, model, providerName, baseUrl
  171. );
  172.  
  173. const headers = this.prepareHeaders(providerName, apiKey);
  174.  
  175. // Log request info (with masked API key)
  176. const apiKeyPreview = `${apiKey.slice(0, 5)}...${apiKey.slice(-4)}`;
  177. this.logger.info(
  178. `[SmolLLM] Request: ${url} | model=${model} | provider=${providerName} | api_key=${apiKeyPreview} | prompt=${prompt.length}`
  179. );
  180.  
  181. // Create an AbortController for timeout handling
  182. const controller = new AbortController();
  183. const timeoutId = setTimeout(() => {
  184. controller.abort();
  185. }, timeout);
  186.  
  187. try {
  188. const response = await fetch(url, {
  189. method: 'POST',
  190. headers: headers,
  191. body: JSON.stringify(data),
  192. signal: controller.signal
  193. });
  194.  
  195. if (!response.ok) {
  196. throw new Error(`HTTP error ${response.status}: ${await response.text() || 'Unknown error'}`);
  197. }
  198.  
  199. // Reset buffer before starting new stream processing
  200. this.buffer = '';
  201. // Handle streaming response
  202. const reader = response.body.getReader();
  203. const decoder = new TextDecoder();
  204. let fullText = '';
  205.  
  206. while (true) {
  207. const { done, value } = await reader.read();
  208. if (done) break;
  209. const chunk = decoder.decode(value, { stream: true });
  210. const deltas = this.processStreamChunks(chunk, providerName);
  211. for (const delta of deltas) {
  212. if (delta) {
  213. fullText += delta;
  214. if (handler) handler(delta, fullText);
  215. }
  216. }
  217. }
  218.  
  219. // Process any remaining buffer content
  220. if (this.buffer) {
  221. this.logger.log(`Processing remaining buffer: ${this.buffer}`);
  222. const deltas = this.processStreamChunks('\n', providerName); // Force processing any remaining buffer
  223. for (const delta of deltas) {
  224. if (delta) {
  225. fullText += delta;
  226. if (handler) handler(delta, fullText);
  227. }
  228. }
  229. }
  230.  
  231. clearTimeout(timeoutId);
  232. return fullText;
  233. } catch (error) {
  234. clearTimeout(timeoutId);
  235. if (error.name === 'AbortError') {
  236. throw new Error(`Request timed out after ${timeout}ms`);
  237. }
  238. throw error;
  239. }
  240. }
  241.  
  242. /**
  243. * Process stream chunks and extract text content
  244. *
  245. * @param {string} chunk - Raw stream chunk
  246. * @param {string} providerName - Provider name
  247. * @returns {Array<string>} - Array of extracted text deltas
  248. */
  249. processStreamChunks(chunk, providerName) {
  250. const deltas = [];
  251. // Add chunk to buffer
  252. this.buffer += chunk;
  253. // Split buffer by newlines
  254. const lines = this.buffer.split('\n');
  255. // Keep the last line in the buffer (might be incomplete)
  256. this.buffer = lines.pop() || '';
  257. for (const line of lines) {
  258. const trimmed = line.trim();
  259. if (!trimmed) continue;
  260. // Check for SSE data prefix
  261. if (trimmed.startsWith('data: ')) {
  262. const data = trimmed.slice(6).trim();
  263. // Skip [DONE] marker
  264. if (data === '[DONE]') continue;
  265. try {
  266. // Parse JSON data
  267. const jsonData = JSON.parse(data);
  268. // Extract text content
  269. const delta = this.extractTextFromChunk(jsonData, providerName);
  270. if (delta) {
  271. deltas.push(delta);
  272. }
  273. } catch (e) {
  274. // Log JSON parse errors but continue processing
  275. if (e instanceof SyntaxError) {
  276. this.logger.log(`Incomplete or invalid JSON: ${data}`);
  277. } else {
  278. this.logger.error(`Error processing chunk: ${e.message}, chunk: ${data}`);
  279. }
  280. }
  281. }
  282. }
  283. return deltas;
  284. }
  285. }
  286.  
  287. // Make it available globally
  288. window.SmolLLM = SmolLLM;
  289.  
  290. // Export for module systems if needed
  291. if (typeof module !== 'undefined') {
  292. module.exports = SmolLLM;
  293. }