kiku-agent 1.0.11 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/README.md +300 -495
  2. package/package.json +12 -11
  3. package/public/fonts/GoogleSans-Bold.ttf +0 -0
  4. package/public/fonts/GoogleSans-Bold.woff2 +0 -0
  5. package/public/fonts/GoogleSans-BoldItalic.ttf +0 -0
  6. package/public/fonts/GoogleSans-Italic.ttf +0 -0
  7. package/public/fonts/GoogleSans-Latin-400.woff2 +0 -0
  8. package/public/fonts/GoogleSans-Latin-500.woff2 +0 -0
  9. package/public/fonts/GoogleSans-Latin-600.woff2 +0 -0
  10. package/public/fonts/GoogleSans-Latin-700.woff2 +0 -0
  11. package/public/fonts/GoogleSans-Medium.ttf +0 -0
  12. package/public/fonts/GoogleSans-Medium.woff2 +0 -0
  13. package/public/fonts/GoogleSans-Regular.ttf +0 -0
  14. package/public/fonts/GoogleSans-Regular.woff2 +0 -0
  15. package/public/fonts/GoogleSans-Thai-400.woff2 +0 -0
  16. package/public/fonts/GoogleSans-Thai-500.woff2 +0 -0
  17. package/public/fonts/GoogleSans-Thai-600.woff2 +0 -0
  18. package/public/fonts/GoogleSans-Thai-700.woff2 +0 -0
  19. package/public/fonts/NotoSansThai-Bold.woff2 +0 -0
  20. package/public/fonts/NotoSansThai-Regular.woff2 +0 -0
  21. package/public/fonts/google-sans-official.css +900 -0
  22. package/public/images/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
  23. package/public/images/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
  24. package/public/images/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
  25. package/public/images/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
  26. package/public/images/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
  27. package/public/images/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
  28. package/public/images/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
  29. package/public/images/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
  30. package/public/images/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
  31. package/public/images/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
  32. package/public/images/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
  33. package/public/images/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
  34. package/public/images/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
  35. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-399915.jpg +0 -0
  36. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-424041.jpg +0 -0
  37. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-588166.jpg +0 -0
  38. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-672198.jpg +0 -0
  39. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-700522.jpg +0 -0
  40. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-839722.jpg +0 -0
  41. package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026825-460586.jpg +0 -0
  42. package/public/images/douyin-update/thumbs//346/212/226/351/237/263202694-689407.jpg +0 -0
  43. package/public/images/douyin-update//346/212/226/351/237/2632026820-399915.mp4 +0 -0
  44. package/public/images/douyin-update//346/212/226/351/237/2632026820-424041.mp4 +0 -0
  45. package/public/images/douyin-update//346/212/226/351/237/2632026820-588166.mp4 +0 -0
  46. package/public/images/douyin-update//346/212/226/351/237/2632026820-672198.mp4 +0 -0
  47. package/public/images/douyin-update//346/212/226/351/237/2632026820-700522.mp4 +0 -0
  48. package/public/images/douyin-update//346/212/226/351/237/2632026820-839722.mp4 +0 -0
  49. package/public/images/douyin-update//346/212/226/351/237/2632026825-460586.mp4 +0 -0
  50. package/public/images/douyin-update//346/212/226/351/237/263202694-689407.mp4 +0 -0
  51. package/public/images/general/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
  52. package/public/images/general/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
  53. package/public/images/general/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
  54. package/public/images/general/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
  55. package/public/images/general/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
  56. package/public/images/general/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
  57. package/public/images/general/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
  58. package/public/images/general/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
  59. package/public/images/general/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
  60. package/public/images/general/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
  61. package/public/images/general/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
  62. package/public/images/general/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
  63. package/public/images/general/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
  64. package/public/images/general/jujingyi_portrait.jpg +0 -0
  65. package/public/images/jujingyi_portrait.jpg +0 -0
  66. package/public/images/series/a-millenniums-wait/poster.jpg +0 -0
  67. package/public/images/series/beauty-of-resilience/poster.jpg +0 -0
  68. package/public/images/series/detective-samoyeds/poster.jpg +0 -0
  69. package/public/images/series/everyday/poster.jpg +0 -0
  70. package/public/images/series/fu-rong-ancient-painting/poster.jpg +0 -0
  71. package/public/images/series/in-a-class-of-her-own/poster.jpg +0 -0
  72. package/public/images/series/in-blossom/poster.jpg +0 -0
  73. package/public/images/series/legend-of-yunxi/poster.jpg +0 -0
  74. package/public/images/series/lets-fight/poster-2.jpg +0 -0
  75. package/public/images/series/lets-fight/poster.jpg +0 -0
  76. package/public/images/series/love-emergency/poster.jpg +0 -0
  77. package/public/images/series/love-under-the-full-moon/poster.jpg +0 -0
  78. package/public/images/series/money-bastion/poster.jpg +0 -0
  79. package/public/images/series/mr-swimmer/poster.jpg +0 -0
  80. package/public/images/series/novoland-castle-in-the-sky/poster.jpg +0 -0
  81. package/public/images/series/please-give-me-a-pair-of-wings/poster.jpg +0 -0
  82. package/public/images/series/rebirth-for-you/poster.jpg +0 -0
  83. package/public/images/series/sword-and-fairy-4/poster.jpg +0 -0
  84. package/public/images/series/the-blooms-at-ruyi-pavilion/poster.jpg +0 -0
  85. package/public/images/series/the-eternal-fragrance/poster-2.jpg +0 -0
  86. package/public/images/series/the-eternal-fragrance/poster.jpg +0 -0
  87. package/public/images/series/the-fallen-flowers/poster.jpg +0 -0
  88. package/public/images/series/the-legend-of-white-snake/poster.jpg +0 -0
  89. package/public/images/series/veil-of-shadows/poster.jpg +0 -0
  90. package/public/images/series/wanhua-sin/poster-2.jpg +0 -0
  91. package/public/images/series/wanhua-sin/poster.jpg +0 -0
  92. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj10yogj62u03s0npe02.jpg +0 -0
  93. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj2pdvyj61li25jkjl02.jpg +0 -0
  94. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj71177j62672w9e8302.jpg +0 -0
  95. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj7twpfj62192197rw02.jpg +0 -0
  96. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj8jfu8j62c03401kx02.jpg +0 -0
  97. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj9dhdsj622k2l77wh02.jpg +0 -0
  98. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkja31s4j62bw3407r802.jpg +0 -0
  99. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjcf331j61402ey1kx02.jpg +0 -0
  100. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjdx8gqj62602pihdt02.jpg +0 -0
  101. package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjfvxnzj62432n57wh02.jpg +0 -0
  102. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixniv2yj627y4fxb2b02.jpg +0 -0
  103. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixsvbdaj625k2vfu1102.jpg +0 -0
  104. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixxxdaoj62c03501l202.jpg +0 -0
  105. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixzeef9j62c0340npd02.jpg +0 -0
  106. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy0vs6tj626t4rbhdu02.jpg +0 -0
  107. package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy21wwuj62bt34hx6q02.jpg +0 -0
  108. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly4wr85j62z629y7wh02.jpg +0 -0
  109. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly67oilj623u2t5npd02.jpg +0 -0
  110. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly9qtw8j62c0340u0x02.jpg +0 -0
  111. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyem4m4j63402c01kz02.jpg +0 -0
  112. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlygkoq4j627s2yeavd02.jpg +0 -0
  113. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyjh0daj62bw340nm102.jpg +0 -0
  114. package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlylejlhj62bw3401kx02.jpg +0 -0
  115. package/public/images/weibo-update/today/0040jbadgy1igaiaa8b7fj627j2y67wh02.jpg +0 -0
  116. package/public/images/weibo-update/today/0040jbadgy1igaiagw3ezj61s02ebu0x02.jpg +0 -0
  117. package/public/images/weibo-update/today/0040jbadgy1igaiaivng3j61rv2ebx6p02.jpg +0 -0
  118. package/public/images/weibo-update/today/0040jbadgy1igaiajuixvj62bw340qv502.jpg +0 -0
  119. package/public/images/weibo-update/today/0040jbadgy1igaiapyartj62bw340hdt02.jpg +0 -0
  120. package/public/images/weibo-update/today/0040jbadgy1igaibkwu4ij61vy2iq1kx02.jpg +0 -0
  121. package/public/images/weibo-update/today/0040jbadgy1igaic1fvkpj61u92eo1ky02.jpg +0 -0
  122. package/public/images/weibo-update/today/0040jbadgy1igaic3cruhj62822ywb2902.jpg +0 -0
  123. package/public/manifest.json +25 -0
  124. package/public/mcp-logo-128.png +0 -0
  125. package/public/mcp-logo-64.png +0 -0
  126. package/public/mcp-logo.svg +16 -0
  127. package/server/agent/agentCore.js +542 -74
  128. package/server/agent/appPaths.js +1 -0
  129. package/server/agent/audioPlayer.js +4 -2
  130. package/server/agent/citations.js +294 -0
  131. package/server/agent/clarificationAnalytics.js +477 -0
  132. package/server/agent/clarifyingQuestions.js +1089 -0
  133. package/server/agent/cli.js +549 -125
  134. package/server/agent/commandFeatures.js +1 -0
  135. package/server/agent/commandRegistry.js +946 -879
  136. package/server/agent/commands.js +546 -272
  137. package/server/agent/config.js +581 -412
  138. package/server/agent/contextCompactor.js +2 -2
  139. package/server/agent/debate.js +235 -0
  140. package/server/agent/eventBus.js +3 -0
  141. package/server/agent/feedbackLearning.js +217 -0
  142. package/server/agent/llmClient.js +512 -394
  143. package/server/agent/mcpClient.js +182 -32
  144. package/server/agent/memory.js +283 -224
  145. package/server/agent/modelManager.js +79 -39
  146. package/server/agent/profiles.js +178 -0
  147. package/server/agent/publicTools.js +22 -0
  148. package/server/agent/queryDetails.js +68 -8
  149. package/server/agent/quizData.js +398 -384
  150. package/server/agent/research.js +121 -79
  151. package/server/agent/sanitizer.js +104 -13
  152. package/server/agent/session.js +319 -11
  153. package/server/agent/subagent.js +8 -3
  154. package/server/agent/suggestionTui.js +294 -0
  155. package/server/agent/suggestions.js +69 -3
  156. package/server/agent/telemetry.js +119 -0
  157. package/server/agent/terminalImage.js +146 -27
  158. package/server/agent/theme.js +17 -0
  159. package/server/agent/toolRouter.js +16 -1
  160. package/server/agent/tools.js +424 -225
  161. package/server/agent/ui.js +486 -64
  162. package/server/agent/version.js +2 -4
  163. package/server/agent/webSearch.js +111 -11
  164. package/server/agent/youtubeAuth.js +24 -6
  165. package/server/apiRouter.js +4222 -0
  166. package/server/mcp/data/discography.js +25 -16
  167. package/server/mcp/data/gallery.js +308 -20
  168. package/server/mcp/data/profile.js +74 -44
  169. package/server/mcp/data/timeline.js +1 -1
  170. package/server/mcp/data/works.js +51 -17
  171. package/server/mcp/dataAdapter.js +5 -3
  172. package/server/mcp/handler.js +5 -11
  173. package/server/mcp/prompts.js +86 -0
  174. package/server/mcp/security.js +18 -15
  175. package/server/mcp/server.js +7 -2
  176. package/server/mcp/tools.js +66 -2
  177. package/server/web-chat/core/browserLauncher.js +18 -0
  178. package/server/web-chat/core/connection.js +194 -0
  179. package/server/web-chat/core/conversationContext.js +82 -0
  180. package/server/web-chat/core/directCatalogEngine.js +196 -0
  181. package/server/web-chat/core/dramaKnowledge.js +142 -0
  182. package/server/web-chat/core/errorHandler.js +51 -0
  183. package/server/web-chat/core/followUpEngine.js +182 -0
  184. package/server/web-chat/core/futureEventGuard.js +80 -0
  185. package/server/web-chat/core/memorySnapshot.js +12 -0
  186. package/server/web-chat/core/security.js +146 -0
  187. package/server/web-chat/core/toolInspector.js +52 -0
  188. package/server/web-chat/router.js +120 -0
  189. package/server/web-chat/routes/chatRoute.js +397 -0
  190. package/server/web-chat/routes/clarifyConfigRoute.js +98 -0
  191. package/server/web-chat/routes/clarifyRoute.js +83 -0
  192. package/server/web-chat/routes/compareRoute.js +79 -0
  193. package/server/web-chat/routes/exportRoute.js +59 -0
  194. package/server/web-chat/routes/feedbackRoute.js +57 -0
  195. package/server/web-chat/routes/newsRoute.js +33 -0
  196. package/server/web-chat/routes/quizRoute.js +11 -0
  197. package/server/web-chat/routes/staticRoute.js +103 -0
  198. package/server/web-chat/routes/statusRoute.js +52 -0
  199. package/server/web-chat/routes/suggestRoute.js +77 -0
  200. package/server/web-chat/routes/timelineRoute.js +34 -0
  201. package/server/web-chat/routes/verifyRoute.js +57 -0
  202. package/server/web-chat/server.js +116 -0
  203. package/server/web-chat/ui/htmlBuilder.js +91 -0
  204. package/server/web-chat/ui/previews/agentBubble.preview.html +134 -0
  205. package/server/web-chat/ui/scripts/agentBubbleSystem.js +320 -0
  206. package/server/web-chat/ui/scripts/client.js +8397 -0
  207. package/server/web-chat/ui/scripts/kikuDb.js +177 -0
  208. package/server/web-chat/ui/scripts/suggestionsData.js +118 -0
  209. package/server/web-chat/ui/scripts/webchatI18n.js +291 -0
  210. package/server/web-chat/ui/styles/main.css +8568 -0
  211. package/server/web-chat/ui/templates/body.html +796 -0
  212. package/server/webChatHtml.js +1 -0
  213. package/server/webChatServer.js +6 -0
  214. package/src/data/discographyData.js +892 -866
  215. package/src/data/galleryData.js +1805 -561
  216. package/src/data/profileData.js +79 -2
  217. package/src/data/socialUpdatesData.js +106 -47
  218. package/src/data/timelineData.js +1 -1
  219. package/src/data/worksData.js +25 -4
  220. package/tokens.css +97 -0
  221. package/server/agent/updateManager.js +0 -456
@@ -1,394 +1,512 @@
1
- import { config, CLOUD_PROVIDERS, normalizeProviderModel, savePersistedProviderKey, savePersistedActiveProvider, savePersistedProviderModel } from './config.js';
2
- import { parseApiErrorText } from './errors.js';
3
-
4
- export class LLMClient {
5
- constructor(options = {}) {
6
- const providerId = (options.provider || config.activeProvider || 'lmstudio').toLowerCase();
7
- const providerDef = CLOUD_PROVIDERS[providerId];
8
- if (!providerDef) {
9
- throw new Error(`Unknown provider "${providerId}". Supported: ${Object.keys(CLOUD_PROVIDERS).join(', ')}`);
10
- }
11
-
12
- this.provider = providerId;
13
- this.providerName = providerDef.name || providerId;
14
- this.baseUrl = (options.baseUrl || providerDef.baseUrl || config.lmStudio.baseUrl).replace(/\/+$/, '');
15
- this.apiKey = options.apiKey !== undefined
16
- ? options.apiKey
17
- : (providerDef.apiKey !== undefined ? providerDef.apiKey : (providerDef.isLocal ? (config.lmStudio.apiKey || 'lm-studio') : ''));
18
- this.model = normalizeProviderModel(
19
- providerId,
20
- options.model || providerDef.defaultModel || config.lmStudio.model || 'google/gemma-4-e4b'
21
- );
22
- this.timeout = options.timeout || config.lmStudio.requestTimeoutMs || 180000;
23
- this.extraHeaders = { ...(providerDef.extraHeaders || {}), ...(options.extraHeaders || {}) };
24
- }
25
-
26
- switchProvider(providerId, options = {}) {
27
- const pId = String(providerId || '').toLowerCase().trim();
28
- const providerDef = CLOUD_PROVIDERS[pId];
29
- if (!providerDef) {
30
- throw new Error(`Unknown provider "${providerId}". Supported: ${Object.keys(CLOUD_PROVIDERS).join(', ')}`);
31
- }
32
-
33
- this.provider = pId;
34
- this.providerName = providerDef.name || pId;
35
- this.baseUrl = (options.baseUrl || providerDef.baseUrl).replace(/\/+$/, '');
36
- this.apiKey = options.apiKey !== undefined ? options.apiKey : (providerDef.apiKey || '');
37
- this.model = normalizeProviderModel(pId, options.model || providerDef.defaultModel);
38
- this.extraHeaders = { ...(providerDef.extraHeaders || {}), ...(options.extraHeaders || {}) };
39
-
40
- savePersistedActiveProvider(pId, this.model);
41
- return this.getProviderInfo();
42
- }
43
-
44
- setApiKey(apiKey, providerId = null) {
45
- const targetProvider = String(providerId || this.provider).toLowerCase().trim();
46
- const cleanKey = String(apiKey || '').trim();
47
- savePersistedProviderKey(targetProvider, cleanKey);
48
- if (targetProvider === this.provider) {
49
- this.apiKey = cleanKey;
50
- }
51
- return this.getProviderInfo();
52
- }
53
-
54
- setModel(model) {
55
- const trimmed = String(model || '').trim();
56
- if (!trimmed) throw new Error('A model ID is required.');
57
- this.model = normalizeProviderModel(this.provider, trimmed);
58
- savePersistedProviderModel(this.provider, this.model);
59
- return this.model;
60
- }
61
-
62
- getProviderInfo() {
63
- const def = CLOUD_PROVIDERS[this.provider] || {};
64
- return {
65
- provider: this.provider,
66
- providerName: this.providerName,
67
- baseUrl: this.baseUrl,
68
- model: this.model,
69
- hasApiKey: Boolean(this.apiKey && this.apiKey !== 'lm-studio'),
70
- isLocal: def.isLocal ?? false,
71
- envKey: def.envKey || null,
72
- docUrl: def.docUrl || null,
73
- apiRefUrl: def.apiRefUrl || null,
74
- pricingSummary: def.pricingSummary || null,
75
- features: def.features || [],
76
- recommendedModels: def.recommendedModels || [],
77
- };
78
- }
79
-
80
- _buildHeaders() {
81
- const headers = {
82
- 'Content-Type': 'application/json',
83
- ...this.extraHeaders,
84
- };
85
-
86
- if (this.apiKey) {
87
- headers['Authorization'] = `Bearer ${this.apiKey}`;
88
- if (this.provider === 'gemini') {
89
- headers['x-goog-api-key'] = this.apiKey;
90
- }
91
- } else if (this.provider === 'lmstudio') {
92
- headers['Authorization'] = 'Bearer lm-studio';
93
- }
94
-
95
- return headers;
96
- }
97
-
98
- async checkConnection() {
99
- const def = CLOUD_PROVIDERS[this.provider] || {};
100
- const startTime = Date.now();
101
-
102
- if (!def.isLocal && !this.apiKey) {
103
- return {
104
- connected: false,
105
- provider: this.provider,
106
- providerName: this.providerName,
107
- error: `No API key configured for ${this.providerName}. Set ${def.envKey || 'API_KEY'} in .env or run /provider setkey <key>`,
108
- models: [],
109
- latencyMs: 0
110
- };
111
- }
112
-
113
- try {
114
- const controller = new AbortController();
115
- const timeoutId = setTimeout(() => controller.abort(), 8000);
116
-
117
- const res = await fetch(`${this.baseUrl}/models`, {
118
- method: 'GET',
119
- headers: this._buildHeaders(),
120
- signal: controller.signal
121
- });
122
-
123
- clearTimeout(timeoutId);
124
- const latencyMs = Date.now() - startTime;
125
-
126
- if (!res.ok) {
127
- let errSnippet = res.statusText;
128
- try {
129
- const errBody = await res.text();
130
- if (errBody) {
131
- errSnippet = parseApiErrorText(errBody);
132
- }
133
- } catch { }
134
-
135
- let formattedError = `${this.providerName} returned status ${res.status}: ${errSnippet}`;
136
- if (res.status === 401) {
137
- formattedError = `401 Unauthorized: Invalid API key for ${this.providerName}. Check ${def.envKey || 'API key'}.`;
138
- } else if (res.status === 402) {
139
- formattedError = `402 Insufficient credits for ${this.providerName}: ${errSnippet}`;
140
- } else if (res.status === 429) {
141
- formattedError = `429 Rate Limit / Quota Exceeded: ${errSnippet}`;
142
- }
143
-
144
- return {
145
- connected: false,
146
- provider: this.provider,
147
- providerName: this.providerName,
148
- error: formattedError,
149
- models: [],
150
- latencyMs
151
- };
152
- }
153
-
154
- const data = await res.json();
155
- const rawList = Array.isArray(data) ? data : (data.data || []);
156
- const models = rawList.map(m => typeof m === 'string' ? m : (m.id || m.name)).filter(Boolean);
157
-
158
- const matchedGemma = models.find(m =>
159
- m === 'google/gemma-4-e4b' ||
160
- m === 'gemma-4-e4b' ||
161
- m.toLowerCase().includes('gemma-4-e4b') ||
162
- (m.toLowerCase().includes('gemma') && m.toLowerCase().includes('e4b'))
163
- );
164
-
165
- const isAvailable = models.length === 0 ? true : models.includes(this.model);
166
-
167
- return {
168
- connected: true,
169
- provider: this.provider,
170
- providerName: this.providerName,
171
- baseUrl: this.baseUrl,
172
- models,
173
- activeModel: models.includes(this.model) ? this.model : (matchedGemma || this.model),
174
- isModelAvailable: isAvailable,
175
- latencyMs
176
- };
177
- } catch (err) {
178
- const latencyMs = Date.now() - startTime;
179
- return {
180
- connected: false,
181
- provider: this.provider,
182
- providerName: this.providerName,
183
- error: err.name === 'AbortError'
184
- ? `Connection timed out at ${this.baseUrl} (>8s)`
185
- : `Cannot reach ${this.providerName} at ${this.baseUrl} (${err.message})`,
186
- models: [],
187
- latencyMs
188
- };
189
- }
190
- }
191
-
192
- async chat({
193
- messages,
194
- tools = null,
195
- temperature = 0.3,
196
- maxTokens = 1536,
197
- stream = true,
198
- signal = null,
199
- onToken = null,
200
- onThought = null
201
- }) {
202
- const isLegacyReasoningModel = /^(o1|o3|deepseek-reasoner)/i.test(this.model) || this.model.includes('reasoner');
203
- const usesCompletionTokenLimit = /^(o1|o3|gpt-5(?:\.|-|$))/i.test(this.model);
204
- const rejectsTemperature = /^(o1|o3|gpt-5(?:\.|-|$))/i.test(this.model);
205
-
206
- const buildPayload = (includeTools = true, includeTemp = true) => {
207
- const p = {
208
- model: this.model,
209
- messages,
210
- stream
211
- };
212
-
213
- if (!rejectsTemperature && includeTemp && temperature !== undefined && temperature !== null) {
214
- p.temperature = temperature;
215
- }
216
-
217
- if (usesCompletionTokenLimit) {
218
- p.max_completion_tokens = maxTokens;
219
- } else {
220
- p.max_tokens = maxTokens;
221
- }
222
-
223
- if (includeTools && tools && Array.isArray(tools) && tools.length > 0 && !isLegacyReasoningModel) {
224
- p.tools = tools;
225
- p.tool_choice = "auto";
226
- }
227
-
228
- return p;
229
- };
230
-
231
- let payload = buildPayload(true, true);
232
-
233
- const controller = new AbortController();
234
- let timeoutId = setTimeout(() => controller.abort(), this.timeout);
235
-
236
- const abortHandler = () => controller.abort();
237
- if (signal) signal.addEventListener('abort', abortHandler, { once: true });
238
-
239
- const resetTimeout = () => {
240
- clearTimeout(timeoutId);
241
- timeoutId = setTimeout(() => controller.abort(), this.timeout);
242
- };
243
-
244
- try {
245
- let res = await fetch(`${this.baseUrl}/chat/completions`, {
246
- method: 'POST',
247
- headers: this._buildHeaders(),
248
- body: JSON.stringify(payload),
249
- signal: controller.signal
250
- });
251
-
252
- resetTimeout();
253
-
254
-
255
- if (!res.ok && res.status === 400) {
256
- const errText = await res.text();
257
- const lowerErr = errText.toLowerCase();
258
- let retryNeeded = false;
259
-
260
- if ((lowerErr.includes('tool') || lowerErr.includes('function') || lowerErr.includes('schema')) && payload.tools) {
261
- payload = buildPayload(false, true);
262
- retryNeeded = true;
263
- } else if (lowerErr.includes('temperature') && payload.temperature !== undefined) {
264
- payload = buildPayload(Boolean(payload.tools), false);
265
- retryNeeded = true;
266
- } else if (lowerErr.includes('max_tokens') && payload.max_tokens !== undefined) {
267
- delete payload.max_tokens;
268
- payload.max_completion_tokens = maxTokens;
269
- retryNeeded = true;
270
- }
271
-
272
- if (retryNeeded) {
273
- res = await fetch(`${this.baseUrl}/chat/completions`, {
274
- method: 'POST',
275
- headers: this._buildHeaders(),
276
- body: JSON.stringify(payload),
277
- signal: controller.signal
278
- });
279
- resetTimeout();
280
- }
281
-
282
- if (!res.ok) {
283
- const finalErr = retryNeeded ? await res.text() : errText;
284
- const cleanMsg = parseApiErrorText(finalErr);
285
- const err = new Error(`${this.providerName} error (${res.status}): ${cleanMsg}`);
286
- err.status = res.status;
287
- err.provider = this.provider;
288
- err.providerName = this.providerName;
289
- err.rawError = finalErr;
290
- throw err;
291
- }
292
- } else if (!res.ok) {
293
- const errorText = await res.text();
294
- const cleanMsg = parseApiErrorText(errorText);
295
- const err = new Error(`${this.providerName} error (${res.status}): ${cleanMsg}`);
296
- err.status = res.status;
297
- err.provider = this.provider;
298
- err.providerName = this.providerName;
299
- err.rawError = errorText;
300
- throw err;
301
- }
302
-
303
- if (!stream) {
304
- const data = await res.json();
305
- clearTimeout(timeoutId);
306
- if (signal) signal.removeEventListener('abort', abortHandler);
307
- const msg = data.choices && data.choices[0] ? data.choices[0].message : null;
308
- if (msg && !msg.reasoning_content && (msg.reasoning || msg.thinking)) {
309
- msg.reasoning_content = msg.reasoning || msg.thinking;
310
- }
311
- return msg;
312
- }
313
-
314
- const reader = res.body.getReader();
315
- const decoder = new TextDecoder('utf-8');
316
- let fullContent = '';
317
- let fullReasoning = '';
318
- let toolCalls = [];
319
- let buffer = '';
320
-
321
- while (true) {
322
- const { done, value } = await reader.read();
323
- if (done) break;
324
-
325
- resetTimeout();
326
-
327
- buffer += decoder.decode(value, { stream: true });
328
- const lines = buffer.split('\n');
329
- buffer = lines.pop() || '';
330
-
331
- for (const line of lines) {
332
- const trimmed = line.trim();
333
- if (!trimmed || !trimmed.startsWith('data:')) continue;
334
- const jsonStr = trimmed.replace(/^data:\s*/, '');
335
- if (jsonStr === '[DONE]') continue;
336
-
337
- try {
338
- const parsed = JSON.parse(jsonStr);
339
- const delta = parsed.choices?.[0]?.delta;
340
- if (!delta) continue;
341
-
342
- const reasoning = delta.reasoning_content || delta.reasoning || delta.thinking;
343
- if (reasoning) {
344
- fullReasoning += reasoning;
345
- if (onThought) onThought(reasoning);
346
- }
347
-
348
- if (delta.content) {
349
- fullContent += delta.content;
350
- if (onToken) onToken(delta.content);
351
- }
352
-
353
- if (delta.tool_calls) {
354
- for (const tc of delta.tool_calls) {
355
- const idx = tc.index ?? 0;
356
- if (!toolCalls[idx]) {
357
- toolCalls[idx] = {
358
- id: tc.id || `call_${Date.now()}_${idx}`,
359
- type: 'function',
360
- function: { name: '', arguments: '' }
361
- };
362
- }
363
- if (tc.function?.name) toolCalls[idx].function.name += tc.function.name;
364
- if (tc.function?.arguments) toolCalls[idx].function.arguments += tc.function.arguments;
365
- }
366
- }
367
- } catch {
368
- }
369
- }
370
- }
371
-
372
- clearTimeout(timeoutId);
373
-
374
- const resultMessage = {
375
- role: 'assistant',
376
- content: fullContent,
377
- reasoning_content: fullReasoning
378
- };
379
-
380
- if (toolCalls.length > 0) {
381
- resultMessage.tool_calls = toolCalls.filter(Boolean);
382
- }
383
-
384
- if (signal) signal.removeEventListener('abort', abortHandler);
385
- return resultMessage;
386
- } catch (err) {
387
- clearTimeout(timeoutId);
388
- if (signal) signal.removeEventListener('abort', abortHandler);
389
- throw err;
390
- }
391
- }
392
- }
393
-
394
- export { LLMClient as LMStudioClient };
1
+ import { config, CLOUD_PROVIDERS, normalizeProviderModel, matchModelIdentifier, extractModelQuantization, savePersistedProviderKey, savePersistedActiveProvider, savePersistedProviderModel, getPersistedActiveProvider } from './config.js';
2
+ import { parseApiErrorText } from './errors.js';
3
+ import { estimateTokens } from './telemetry.js';
4
+
5
+ export class LLMClient {
6
+ constructor(options = {}) {
7
+ const providerId = (options.provider || getPersistedActiveProvider() || config.activeProvider || 'lmstudio').toLowerCase();
8
+ const providerDef = CLOUD_PROVIDERS[providerId];
9
+ if (!providerDef) {
10
+ throw new Error(`Unknown provider "${providerId}". Supported: ${Object.keys(CLOUD_PROVIDERS).join(', ')}`);
11
+ }
12
+
13
+ this.provider = providerId;
14
+ this.providerName = providerDef.name || providerId;
15
+ this.baseUrl = (options.baseUrl || providerDef.baseUrl || config.lmStudio.baseUrl).replace(/\/+$/, '');
16
+ this.apiKey = options.apiKey !== undefined
17
+ ? options.apiKey
18
+ : (providerDef.apiKey !== undefined ? providerDef.apiKey : (providerDef.isLocal ? (config.lmStudio.apiKey || 'lm-studio') : ''));
19
+ this.model = normalizeProviderModel(
20
+ providerId,
21
+ options.model || providerDef.defaultModel || config.lmStudio.model || 'google/gemma-4-e4b'
22
+ );
23
+ this.timeout = options.timeout || config.lmStudio.requestTimeoutMs || 180000;
24
+ this.extraHeaders = { ...(providerDef.extraHeaders || {}), ...(options.extraHeaders || {}) };
25
+ }
26
+
27
+ switchProvider(providerId, options = {}) {
28
+ const pId = String(providerId || '').toLowerCase().trim();
29
+ const providerDef = CLOUD_PROVIDERS[pId];
30
+ if (!providerDef) {
31
+ throw new Error(`Unknown provider "${providerId}". Supported: ${Object.keys(CLOUD_PROVIDERS).join(', ')}`);
32
+ }
33
+
34
+ this.provider = pId;
35
+ this.providerName = providerDef.name || pId;
36
+ this.baseUrl = (options.baseUrl || providerDef.baseUrl).replace(/\/+$/, '');
37
+ this.apiKey = options.apiKey !== undefined ? options.apiKey : (providerDef.apiKey || '');
38
+ this.model = normalizeProviderModel(pId, options.model || providerDef.defaultModel);
39
+ this.extraHeaders = { ...(providerDef.extraHeaders || {}), ...(options.extraHeaders || {}) };
40
+
41
+ savePersistedActiveProvider(pId, this.model);
42
+ return this.getProviderInfo();
43
+ }
44
+
45
+ setApiKey(apiKey, providerId = null) {
46
+ const targetProvider = String(providerId || this.provider).toLowerCase().trim();
47
+ const cleanKey = String(apiKey || '').trim();
48
+ savePersistedProviderKey(targetProvider, cleanKey);
49
+ if (targetProvider === this.provider) {
50
+ this.apiKey = cleanKey;
51
+ }
52
+ return this.getProviderInfo();
53
+ }
54
+
55
+ setModel(model) {
56
+ const trimmed = String(model || '').trim();
57
+ if (!trimmed) throw new Error('A model ID is required.');
58
+ this.model = normalizeProviderModel(this.provider, trimmed);
59
+ savePersistedProviderModel(this.provider, this.model);
60
+ return this.model;
61
+ }
62
+
63
+ getProviderInfo() {
64
+ const def = CLOUD_PROVIDERS[this.provider] || {};
65
+ return {
66
+ provider: this.provider,
67
+ providerName: this.providerName,
68
+ baseUrl: this.baseUrl,
69
+ model: this.model,
70
+ hasApiKey: Boolean(this.apiKey && this.apiKey !== 'lm-studio'),
71
+ isLocal: def.isLocal ?? false,
72
+ envKey: def.envKey || null,
73
+ docUrl: def.docUrl || null,
74
+ apiRefUrl: def.apiRefUrl || null,
75
+ pricingSummary: def.pricingSummary || null,
76
+ features: def.features || [],
77
+ recommendedModels: def.recommendedModels || [],
78
+ };
79
+ }
80
+
81
+ _buildHeaders() {
82
+ const headers = {
83
+ 'Content-Type': 'application/json',
84
+ ...this.extraHeaders,
85
+ };
86
+
87
+ if (this.apiKey) {
88
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
89
+ if (this.provider === 'gemini') {
90
+ headers['x-goog-api-key'] = this.apiKey;
91
+ }
92
+ } else if (this.provider === 'lmstudio') {
93
+ headers['Authorization'] = 'Bearer lm-studio';
94
+ }
95
+
96
+ return headers;
97
+ }
98
+
99
+ async checkConnection() {
100
+ const def = CLOUD_PROVIDERS[this.provider] || {};
101
+ const startTime = Date.now();
102
+
103
+ if (!def.isLocal && !this.apiKey) {
104
+ return {
105
+ connected: false,
106
+ provider: this.provider,
107
+ providerName: this.providerName,
108
+ error: `No API key configured for ${this.providerName}. Set ${def.envKey || 'API_KEY'} in .env or run /provider setkey <key>`,
109
+ models: [],
110
+ latencyMs: 0
111
+ };
112
+ }
113
+ try {
114
+ const controller = new AbortController();
115
+ const timeoutMs = process.env.NODE_ENV === 'test' || process.execArgv.includes('--test') ? 800 : 8000;
116
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
117
+
118
+ const res = await fetch(`${this.baseUrl}/models`, {
119
+ method: 'GET',
120
+ headers: this._buildHeaders(),
121
+ signal: controller.signal
122
+ });
123
+
124
+ clearTimeout(timeoutId);
125
+ const latencyMs = Date.now() - startTime;
126
+
127
+ if (!res.ok) {
128
+ let errSnippet = res.statusText;
129
+ try {
130
+ const errBody = await res.text();
131
+ if (errBody) {
132
+ errSnippet = parseApiErrorText(errBody);
133
+ }
134
+ } catch { }
135
+
136
+ let formattedError = `${this.providerName} returned status ${res.status}: ${errSnippet}`;
137
+ if (res.status === 401) {
138
+ formattedError = `401 Unauthorized: Invalid API key for ${this.providerName}. Check ${def.envKey || 'API key'}.`;
139
+ } else if (res.status === 402) {
140
+ formattedError = `402 Insufficient credits for ${this.providerName}: ${errSnippet}`;
141
+ } else if (res.status === 429) {
142
+ formattedError = `429 Rate Limit / Quota Exceeded: ${errSnippet}`;
143
+ }
144
+
145
+ return {
146
+ connected: false,
147
+ provider: this.provider,
148
+ providerName: this.providerName,
149
+ error: formattedError,
150
+ models: [],
151
+ latencyMs
152
+ };
153
+ }
154
+
155
+ const data = await res.json();
156
+ const rawList = Array.isArray(data) ? data : (data.data || []);
157
+ const models = rawList.map(m => typeof m === 'string' ? m : (m.id || m.name)).filter(Boolean);
158
+
159
+
160
+ const loadedModels = rawList
161
+ .filter(m => m && typeof m === 'object' && (m.state === 'loaded' || m.loaded === true || m.status === 'loaded'))
162
+ .map(m => m.id || m.name)
163
+ .filter(Boolean);
164
+
165
+ const activeLoaded = loadedModels.length > 0 ? loadedModels[0] : null;
166
+
167
+
168
+ const exactMatch = models.find(m => m === this.model);
169
+ const fuzzyMatch = models.find(m => matchModelIdentifier(this.model, m));
170
+
171
+
172
+ const matchedGemma = (activeLoaded && /gemma/i.test(activeLoaded))
173
+ ? activeLoaded
174
+ : (fuzzyMatch || models.find(m =>
175
+ m === 'google/gemma-4-e4b' ||
176
+ m === 'gemma-4-e4b' ||
177
+ m.toLowerCase().includes('gemma-4-e4b') ||
178
+ (m.toLowerCase().includes('gemma') && m.toLowerCase().includes('e4b')) ||
179
+ /gemma/i.test(m)
180
+ ));
181
+
182
+ const resolvedActive = exactMatch || (activeLoaded ? activeLoaded : (fuzzyMatch || matchedGemma || this.model));
183
+
184
+
185
+ if (this.provider === 'lmstudio' && models.length > 0) {
186
+ if (exactMatch) {
187
+ this.model = exactMatch;
188
+ } else if (activeLoaded && (this.model === 'google/gemma-4-e4b' || matchModelIdentifier(this.model, activeLoaded))) {
189
+ this.model = activeLoaded;
190
+ } else if (fuzzyMatch) {
191
+ this.model = fuzzyMatch;
192
+ } else if (matchedGemma && this.model === 'google/gemma-4-e4b') {
193
+ this.model = matchedGemma;
194
+ }
195
+ }
196
+
197
+ const isAvailable = models.length === 0 ? true : (
198
+ models.includes(this.model) ||
199
+ models.some(m => matchModelIdentifier(this.model, m)) ||
200
+ Boolean(activeLoaded && models.includes(activeLoaded))
201
+ );
202
+
203
+ return {
204
+ connected: true,
205
+ provider: this.provider,
206
+ providerName: this.providerName,
207
+ baseUrl: this.baseUrl,
208
+ models,
209
+ loadedModels: loadedModels.length > 0 ? loadedModels : (models.length > 0 ? [models[0]] : []),
210
+ activeLoadedModel: activeLoaded,
211
+ activeModel: resolvedActive,
212
+ isModelAvailable: isAvailable,
213
+ latencyMs
214
+ };
215
+ } catch (err) {
216
+ const latencyMs = Date.now() - startTime;
217
+ return {
218
+ connected: false,
219
+ provider: this.provider,
220
+ providerName: this.providerName,
221
+ error: err.name === 'AbortError'
222
+ ? `Connection timed out at ${this.baseUrl} (>8s)`
223
+ : `Cannot reach ${this.providerName} at ${this.baseUrl} (${err.message})`,
224
+ models: [],
225
+ latencyMs
226
+ };
227
+ }
228
+ }
229
+
230
+ async chat({
231
+ messages,
232
+ tools = null,
233
+ temperature = 0.3,
234
+ maxTokens = 8192,
235
+ stream = true,
236
+ signal = null,
237
+ onToken = null,
238
+ onThought = null
239
+ }) {
240
+ const isLegacyReasoningModel = /^(o1|o3|deepseek-reasoner)/i.test(this.model) || this.model.includes('reasoner');
241
+ const usesCompletionTokenLimit = /^(o1|o3|gpt-5(?:\.|-|$))/i.test(this.model);
242
+ const rejectsTemperature = /^(o1|o3|gpt-5(?:\.|-|$))/i.test(this.model);
243
+
244
+ const buildPayload = (includeTools = true, includeTemp = true) => {
245
+ const p = {
246
+ model: this.model,
247
+ messages,
248
+ stream
249
+ };
250
+
251
+ if (!rejectsTemperature && includeTemp && temperature !== undefined && temperature !== null) {
252
+ p.temperature = temperature;
253
+ }
254
+
255
+ if (usesCompletionTokenLimit) {
256
+ p.max_completion_tokens = maxTokens;
257
+ } else {
258
+ p.max_tokens = maxTokens;
259
+ }
260
+
261
+ if (includeTools && tools && Array.isArray(tools) && tools.length > 0 && !isLegacyReasoningModel) {
262
+ p.tools = tools;
263
+ p.tool_choice = "auto";
264
+ }
265
+
266
+ if (stream) {
267
+ p.stream_options = { include_usage: true };
268
+ }
269
+
270
+ return p;
271
+ };
272
+
273
+ let payload = buildPayload(true, true);
274
+
275
+ const controller = new AbortController();
276
+ let timeoutId = setTimeout(() => controller.abort(), this.timeout);
277
+
278
+ const abortHandler = () => controller.abort();
279
+ if (signal) signal.addEventListener('abort', abortHandler, { once: true });
280
+
281
+ const resetTimeout = () => {
282
+ clearTimeout(timeoutId);
283
+ timeoutId = setTimeout(() => controller.abort(), this.timeout);
284
+ };
285
+
286
+ try {
287
+ let res;
288
+ const maxRetries = 2;
289
+
290
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
291
+ if (controller.signal.aborted) break;
292
+ try {
293
+ res = await fetch(`${this.baseUrl}/chat/completions`, {
294
+ method: 'POST',
295
+ headers: this._buildHeaders(),
296
+ body: JSON.stringify(payload),
297
+ signal: controller.signal
298
+ });
299
+
300
+ resetTimeout();
301
+
302
+ if (res.ok) break;
303
+
304
+ if (res.status === 400) {
305
+ const errText = await res.text();
306
+ const lowerErr = errText.toLowerCase();
307
+ let retryNeeded = false;
308
+
309
+ if ((lowerErr.includes('tool') || lowerErr.includes('function') || lowerErr.includes('schema')) && payload.tools) {
310
+ payload = buildPayload(false, true);
311
+ retryNeeded = true;
312
+ } else if (lowerErr.includes('temperature') && payload.temperature !== undefined) {
313
+ payload = buildPayload(Boolean(payload.tools), false);
314
+ retryNeeded = true;
315
+ } else if (lowerErr.includes('max_tokens') && payload.max_tokens !== undefined) {
316
+ delete payload.max_tokens;
317
+ payload.max_completion_tokens = maxTokens;
318
+ retryNeeded = true;
319
+ } else if (lowerErr.includes('stream_options') && payload.stream_options) {
320
+ delete payload.stream_options;
321
+ retryNeeded = true;
322
+ }
323
+
324
+ if (retryNeeded) {
325
+ res = await fetch(`${this.baseUrl}/chat/completions`, {
326
+ method: 'POST',
327
+ headers: this._buildHeaders(),
328
+ body: JSON.stringify(payload),
329
+ signal: controller.signal
330
+ });
331
+ resetTimeout();
332
+ }
333
+
334
+ if (!res.ok) {
335
+ const finalErr = retryNeeded ? await res.text() : errText;
336
+ const cleanMsg = parseApiErrorText(finalErr);
337
+ const err = new Error(`${this.providerName} error (${res.status}): ${cleanMsg}`);
338
+ err.status = res.status;
339
+ err.provider = this.provider;
340
+ err.providerName = this.providerName;
341
+ err.rawError = finalErr;
342
+ throw err;
343
+ }
344
+ break;
345
+ }
346
+
347
+ if ([429, 500, 502, 503, 504].includes(res.status) && attempt < maxRetries && !controller.signal.aborted) {
348
+ const backoffMs = (attempt + 1) * 1200;
349
+ await new Promise(r => setTimeout(r, backoffMs));
350
+ continue;
351
+ }
352
+
353
+ const errorText = await res.text();
354
+ const cleanMsg = parseApiErrorText(errorText);
355
+ const err = new Error(`${this.providerName} error (${res.status}): ${cleanMsg}`);
356
+ err.status = res.status;
357
+ err.provider = this.provider;
358
+ err.providerName = this.providerName;
359
+ err.rawError = errorText;
360
+ throw err;
361
+ } catch (fetchErr) {
362
+ if (controller.signal.aborted || attempt >= maxRetries || fetchErr.status) {
363
+ throw fetchErr;
364
+ }
365
+ const backoffMs = (attempt + 1) * 1200;
366
+ await new Promise(r => setTimeout(r, backoffMs));
367
+ }
368
+ }
369
+
370
+ if (!res || !res.ok) {
371
+ throw new Error(`${this.providerName} request failed with no valid response.`);
372
+ }
373
+
374
+ if (!stream) {
375
+ const data = await res.json();
376
+ clearTimeout(timeoutId);
377
+ if (signal) signal.removeEventListener('abort', abortHandler);
378
+ const msg = data.choices && data.choices[0] ? data.choices[0].message : null;
379
+ if (msg) {
380
+ if (!msg.reasoning_content && (msg.reasoning || msg.thinking)) {
381
+ msg.reasoning_content = msg.reasoning || msg.thinking;
382
+ }
383
+ if (data.usage) {
384
+ msg.usage = {
385
+ prompt_tokens: data.usage.prompt_tokens || 0,
386
+ completion_tokens: data.usage.completion_tokens || 0,
387
+ reasoning_tokens: data.usage.completion_tokens_details?.reasoning_tokens || 0,
388
+ total_tokens: data.usage.total_tokens || ((data.usage.prompt_tokens || 0) + (data.usage.completion_tokens || 0))
389
+ };
390
+ } else {
391
+ const inputPrompt = messages.map(m => (typeof m.content === 'string' ? m.content : JSON.stringify(m.content || ''))).join('\n');
392
+ const pTokens = estimateTokens(inputPrompt);
393
+ const cTokens = estimateTokens(msg.content || '');
394
+ msg.usage = {
395
+ prompt_tokens: pTokens,
396
+ completion_tokens: cTokens,
397
+ reasoning_tokens: 0,
398
+ total_tokens: pTokens + cTokens
399
+ };
400
+ }
401
+ }
402
+ return msg;
403
+ }
404
+
405
+ const reader = res.body.getReader();
406
+ const decoder = new TextDecoder('utf-8');
407
+ let fullContent = '';
408
+ let fullReasoning = '';
409
+ let toolCalls = [];
410
+ let buffer = '';
411
+ let reportedUsage = null;
412
+
413
+ while (true) {
414
+ const { done, value } = await reader.read();
415
+ if (done) break;
416
+
417
+ resetTimeout();
418
+
419
+ buffer += decoder.decode(value, { stream: true });
420
+ const lines = buffer.split('\n');
421
+ buffer = lines.pop() || '';
422
+
423
+ for (const line of lines) {
424
+ const trimmed = line.trim();
425
+ if (!trimmed || !trimmed.startsWith('data:')) continue;
426
+ const jsonStr = trimmed.replace(/^data:\s*/, '');
427
+ if (jsonStr === '[DONE]') continue;
428
+
429
+ try {
430
+ const parsed = JSON.parse(jsonStr);
431
+ if (parsed.usage) {
432
+ reportedUsage = parsed.usage;
433
+ }
434
+ const delta = parsed.choices?.[0]?.delta;
435
+ if (!delta) continue;
436
+
437
+ const reasoning = delta.reasoning_content || delta.reasoning || delta.thinking;
438
+ if (reasoning) {
439
+ fullReasoning += reasoning;
440
+ if (onThought) onThought(reasoning);
441
+ }
442
+
443
+ if (delta.content) {
444
+ fullContent += delta.content;
445
+ if (onToken) onToken(delta.content);
446
+ }
447
+
448
+ if (delta.tool_calls) {
449
+ for (const tc of delta.tool_calls) {
450
+ const idx = tc.index ?? 0;
451
+ if (!toolCalls[idx]) {
452
+ toolCalls[idx] = {
453
+ id: tc.id || `call_${Date.now()}_${idx}`,
454
+ type: 'function',
455
+ function: { name: '', arguments: '' }
456
+ };
457
+ }
458
+ if (tc.function?.name) toolCalls[idx].function.name += tc.function.name;
459
+ if (tc.function?.arguments) toolCalls[idx].function.arguments += tc.function.arguments;
460
+ }
461
+ }
462
+ } catch {
463
+ }
464
+ }
465
+ }
466
+
467
+ clearTimeout(timeoutId);
468
+
469
+ let promptTokens = reportedUsage?.prompt_tokens ?? 0;
470
+ let completionTokens = reportedUsage?.completion_tokens ?? 0;
471
+ let reasoningTokens = reportedUsage?.completion_tokens_details?.reasoning_tokens ?? 0;
472
+
473
+ if (!promptTokens) {
474
+ const inputPrompt = messages.map(m => (typeof m.content === 'string' ? m.content : JSON.stringify(m.content || ''))).join('\n');
475
+ promptTokens = estimateTokens(inputPrompt);
476
+ }
477
+ if (!completionTokens) {
478
+ completionTokens = estimateTokens(fullContent + (fullReasoning || ''));
479
+ }
480
+ if (!reasoningTokens && fullReasoning) {
481
+ reasoningTokens = estimateTokens(fullReasoning);
482
+ }
483
+
484
+ const totalTokens = reportedUsage?.total_tokens ?? (promptTokens + completionTokens);
485
+
486
+ const resultMessage = {
487
+ role: 'assistant',
488
+ content: fullContent,
489
+ reasoning_content: fullReasoning,
490
+ usage: {
491
+ prompt_tokens: promptTokens,
492
+ completion_tokens: completionTokens,
493
+ reasoning_tokens: reasoningTokens,
494
+ total_tokens: totalTokens
495
+ }
496
+ };
497
+
498
+ if (toolCalls.length > 0) {
499
+ resultMessage.tool_calls = toolCalls.filter(Boolean);
500
+ }
501
+
502
+ if (signal) signal.removeEventListener('abort', abortHandler);
503
+ return resultMessage;
504
+ } catch (err) {
505
+ clearTimeout(timeoutId);
506
+ if (signal) signal.removeEventListener('abort', abortHandler);
507
+ throw err;
508
+ }
509
+ }
510
+ }
511
+
512
+ export { LLMClient as LMStudioClient };