SmolLLM

LLM utility library

目前為 2025-03-04 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.gf.qytechs.cn/scripts/528704/1546875/SmolLLM.js

  1. // ==UserScript==
  2. // @name SmolLLM
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.1.13
  5. // @description LLM utility library
  6. // @author RoCry
  7. // @require https://update.gf.qytechs.cn/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. }
  20.  
  21. /**
  22. * Prepares request data based on the provider
  23. *
  24. * @param {string} prompt - User prompt
  25. * @param {string} systemPrompt - System prompt
  26. * @param {string} modelName - Model name
  27. * @param {string} providerName - Provider name (anthropic, openai, gemini)
  28. * @param {string} baseUrl - API base URL
  29. * @returns {Object} - {url, data} for the request
  30. */
  31. prepareRequestData(prompt, systemPrompt, modelName, providerName, baseUrl) {
  32. let url, data;
  33.  
  34. if (providerName === 'anthropic') {
  35. url = `${baseUrl}/v1/messages`;
  36. data = {
  37. model: modelName,
  38. max_tokens: 4096,
  39. messages: [{ role: 'user', content: prompt }],
  40. stream: true
  41. };
  42. if (systemPrompt) {
  43. data.system = systemPrompt;
  44. }
  45. } else if (providerName === 'gemini') {
  46. url = `${baseUrl}/v1beta/models/${modelName}:streamGenerateContent?alt=sse`;
  47. data = {
  48. contents: [{ parts: [{ text: prompt }] }]
  49. };
  50. if (systemPrompt) {
  51. data.system_instruction = { parts: [{ text: systemPrompt }] };
  52. }
  53. } else {
  54. // OpenAI compatible APIs
  55. const messages = [];
  56. if (systemPrompt) {
  57. messages.push({ role: 'system', content: systemPrompt });
  58. }
  59. messages.push({ role: 'user', content: prompt });
  60.  
  61. data = {
  62. messages: messages,
  63. model: modelName,
  64. stream: true
  65. };
  66.  
  67. // Handle URL based on suffix
  68. if (baseUrl.endsWith('#')) {
  69. url = baseUrl.slice(0, -1); // Remove the # and use exact URL
  70. } else if (baseUrl.endsWith('/')) {
  71. url = `${baseUrl}chat/completions`; // Skip v1 prefix
  72. } else {
  73. url = `${baseUrl}/v1/chat/completions`; // Default pattern
  74. }
  75. }
  76.  
  77. return { url, data };
  78. }
  79.  
  80. prepareHeaders(providerName, apiKey) {
  81. const headers = {
  82. 'Content-Type': 'application/json'
  83. };
  84.  
  85. if (providerName === 'anthropic') {
  86. headers['X-API-Key'] = apiKey;
  87. headers['Anthropic-Version'] = '2023-06-01';
  88. } else if (providerName === 'gemini') {
  89. headers['X-Goog-Api-Key'] = apiKey;
  90. } else {
  91. headers['Authorization'] = `Bearer ${apiKey}`;
  92. }
  93.  
  94. return headers;
  95. }
  96.  
  97. /**
  98. * Process SSE stream data for different providers
  99. *
  100. * @param {string} chunk - Data chunk from SSE
  101. * @param {string} providerName - Provider name
  102. * @returns {string|null} - Extracted text content or null
  103. */
  104. processStreamChunk(chunk, providerName) {
  105. if (!chunk || chunk === '[DONE]') return null;
  106.  
  107. try {
  108. this.logger.log(`Processing chunk for ${providerName}:`, chunk);
  109. const data = JSON.parse(chunk);
  110.  
  111. if (providerName === 'gemini') {
  112. const candidates = data.candidates || [];
  113. if (candidates.length > 0 && candidates[0].content) {
  114. const parts = candidates[0].content.parts;
  115. if (parts && parts.length > 0) {
  116. return parts[0].text || '';
  117. }
  118. } else {
  119. this.logger.log(`No content found in chunk for ${providerName}: ${chunk}`);
  120. return null;
  121. }
  122. } else if (providerName === 'anthropic') {
  123. // Handle content_block_delta which contains the actual text
  124. if (data.type === 'content_block_delta') {
  125. const delta = data.delta || {};
  126. if (delta.type === 'text_delta' || delta.text) {
  127. return delta.text || '';
  128. }
  129. }
  130. // Anthropic sends various event types - only some contain text
  131. return null;
  132. } else {
  133. // OpenAI compatible format
  134. const choice = (data.choices || [{}])[0];
  135. if (choice.finish_reason !== null && choice.finish_reason !== undefined) {
  136. return null; // End of generation
  137. }
  138. return choice.delta && choice.delta.content ? choice.delta.content : null;
  139. }
  140. } catch (e) {
  141. this.logger.error(`Error parsing chunk: ${e.message}, chunk: ${chunk}`);
  142. return null;
  143. }
  144. }
  145.  
  146. /**
  147. * @returns {Promise<string>} - Full final response text
  148. */
  149. async askLLM({
  150. prompt,
  151. providerName,
  152. systemPrompt = '',
  153. model,
  154. apiKey,
  155. baseUrl,
  156. handler = null, // handler(delta, fullText)
  157. timeout = 60000
  158. }) {
  159. if (!prompt || !providerName || !model || !apiKey || !baseUrl) {
  160. throw new Error('Required parameters missing');
  161. }
  162.  
  163. // Use balancer to choose API key and base URL pair
  164. [apiKey, baseUrl] = this.balancer.choosePair(apiKey, baseUrl);
  165.  
  166. const { url, data } = this.prepareRequestData(
  167. prompt, systemPrompt, model, providerName, baseUrl
  168. );
  169.  
  170. const headers = this.prepareHeaders(providerName, apiKey);
  171.  
  172. // Log request info (with masked API key)
  173. const apiKeyPreview = `${apiKey.slice(0, 5)}...${apiKey.slice(-4)}`;
  174. this.logger.info(
  175. `[SmolLLM] Request: ${url} | model=${model} | provider=${providerName} | api_key=${apiKeyPreview} | prompt=${prompt.length}`
  176. );
  177.  
  178. // Create an AbortController for timeout handling
  179. const controller = new AbortController();
  180. const timeoutId = setTimeout(() => {
  181. controller.abort();
  182. }, timeout);
  183.  
  184. try {
  185. const response = await fetch(url, {
  186. method: 'POST',
  187. headers: headers,
  188. body: JSON.stringify(data),
  189. signal: controller.signal
  190. });
  191.  
  192. if (!response.ok) {
  193. throw new Error(`HTTP error ${response.status}: ${await response.text() || 'Unknown error'}`);
  194. }
  195.  
  196. // Handle streaming response
  197. const reader = response.body.getReader();
  198. const decoder = new TextDecoder();
  199. let fullText = '';
  200. let buffer = '';
  201.  
  202. while (true) {
  203. const { done, value } = await reader.read();
  204. if (done) break;
  205. const chunk = decoder.decode(value, { stream: true });
  206. buffer += chunk;
  207. // Process SSE data
  208. this.processSSEChunks(chunk, providerName, (delta) => {
  209. if (delta) {
  210. fullText += delta;
  211. if (handler) handler(delta, fullText);
  212. }
  213. });
  214. }
  215.  
  216. clearTimeout(timeoutId);
  217. return fullText;
  218. } catch (error) {
  219. clearTimeout(timeoutId);
  220. if (error.name === 'AbortError') {
  221. throw new Error(`Request timed out after ${timeout}ms`);
  222. }
  223. throw error;
  224. }
  225. }
  226.  
  227. /**
  228. * Process SSE chunks for different providers
  229. *
  230. * @param {string} text - The SSE text chunk
  231. * @param {string} providerName - Provider name
  232. * @param {Function} callback - Callback function for each delta
  233. */
  234. processSSEChunks(text, providerName, callback) {
  235. // Split the input by newlines
  236. const lines = text.split('\n');
  237. for (let line of lines) {
  238. line = line.trim();
  239. if (!line) continue;
  240. // Check for data prefix
  241. if (line.startsWith('data: ')) {
  242. const data = line.slice(6).trim();
  243. // Skip [DONE] marker
  244. if (data === '[DONE]') continue;
  245. // Process the chunk based on provider
  246. const delta = this.processStreamChunk(data, providerName);
  247. callback(delta);
  248. }
  249. }
  250. }
  251. }
  252.  
  253. // Make it available globally
  254. window.SmolLLM = SmolLLM;
  255.  
  256. // Export for module systems if needed
  257. if (typeof module !== 'undefined') {
  258. module.exports = SmolLLM;
  259. }

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址