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.
- package/README.md +300 -495
- package/package.json +12 -11
- package/public/fonts/GoogleSans-Bold.ttf +0 -0
- package/public/fonts/GoogleSans-Bold.woff2 +0 -0
- package/public/fonts/GoogleSans-BoldItalic.ttf +0 -0
- package/public/fonts/GoogleSans-Italic.ttf +0 -0
- package/public/fonts/GoogleSans-Latin-400.woff2 +0 -0
- package/public/fonts/GoogleSans-Latin-500.woff2 +0 -0
- package/public/fonts/GoogleSans-Latin-600.woff2 +0 -0
- package/public/fonts/GoogleSans-Latin-700.woff2 +0 -0
- package/public/fonts/GoogleSans-Medium.ttf +0 -0
- package/public/fonts/GoogleSans-Medium.woff2 +0 -0
- package/public/fonts/GoogleSans-Regular.ttf +0 -0
- package/public/fonts/GoogleSans-Regular.woff2 +0 -0
- package/public/fonts/GoogleSans-Thai-400.woff2 +0 -0
- package/public/fonts/GoogleSans-Thai-500.woff2 +0 -0
- package/public/fonts/GoogleSans-Thai-600.woff2 +0 -0
- package/public/fonts/GoogleSans-Thai-700.woff2 +0 -0
- package/public/fonts/NotoSansThai-Bold.woff2 +0 -0
- package/public/fonts/NotoSansThai-Regular.woff2 +0 -0
- package/public/fonts/google-sans-official.css +900 -0
- package/public/images/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
- package/public/images/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
- package/public/images/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
- package/public/images/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
- package/public/images/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
- package/public/images/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
- package/public/images/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
- package/public/images/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
- package/public/images/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
- package/public/images/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
- package/public/images/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
- package/public/images/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
- package/public/images/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-399915.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-424041.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-588166.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-672198.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-700522.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026820-839722.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/2632026825-460586.jpg +0 -0
- package/public/images/douyin-update/thumbs//346/212/226/351/237/263202694-689407.jpg +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-399915.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-424041.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-588166.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-672198.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-700522.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026820-839722.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/2632026825-460586.mp4 +0 -0
- package/public/images/douyin-update//346/212/226/351/237/263202694-689407.mp4 +0 -0
- package/public/images/general/0040jbadgy1id6am4yxqfj61s82dn1kx02.jpg +0 -0
- package/public/images/general/0040jbadgy1id6am708pbj62c0340qv602.jpg +0 -0
- package/public/images/general/0040jbadgy1id6amb81uqj62c0340x6q02.jpg +0 -0
- package/public/images/general/0040jbadgy1id6amcqte3j62c0340qv602.jpg +0 -0
- package/public/images/general/0040jbadgy1id6amdg3xcj62b232wqv502.jpg +0 -0
- package/public/images/general/0040jbadgy1id6amexv23j620v2pau0x02.jpg +0 -0
- package/public/images/general/0040jbadly8icq3if7twwj60qy0qy0uc02.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s89nri5j30i20n1gn4.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s8a2lmlj30i20ny409.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s8ay1hsj30i20nk3zv.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s8bab6sj30i20o7go1.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s8blqsxj30i20oiq4r.jpg +0 -0
- package/public/images/general/008ET7Legy1ig9s8cbxkzj30i20mxmzh.jpg +0 -0
- package/public/images/general/jujingyi_portrait.jpg +0 -0
- package/public/images/jujingyi_portrait.jpg +0 -0
- package/public/images/series/a-millenniums-wait/poster.jpg +0 -0
- package/public/images/series/beauty-of-resilience/poster.jpg +0 -0
- package/public/images/series/detective-samoyeds/poster.jpg +0 -0
- package/public/images/series/everyday/poster.jpg +0 -0
- package/public/images/series/fu-rong-ancient-painting/poster.jpg +0 -0
- package/public/images/series/in-a-class-of-her-own/poster.jpg +0 -0
- package/public/images/series/in-blossom/poster.jpg +0 -0
- package/public/images/series/legend-of-yunxi/poster.jpg +0 -0
- package/public/images/series/lets-fight/poster-2.jpg +0 -0
- package/public/images/series/lets-fight/poster.jpg +0 -0
- package/public/images/series/love-emergency/poster.jpg +0 -0
- package/public/images/series/love-under-the-full-moon/poster.jpg +0 -0
- package/public/images/series/money-bastion/poster.jpg +0 -0
- package/public/images/series/mr-swimmer/poster.jpg +0 -0
- package/public/images/series/novoland-castle-in-the-sky/poster.jpg +0 -0
- package/public/images/series/please-give-me-a-pair-of-wings/poster.jpg +0 -0
- package/public/images/series/rebirth-for-you/poster.jpg +0 -0
- package/public/images/series/sword-and-fairy-4/poster.jpg +0 -0
- package/public/images/series/the-blooms-at-ruyi-pavilion/poster.jpg +0 -0
- package/public/images/series/the-eternal-fragrance/poster-2.jpg +0 -0
- package/public/images/series/the-eternal-fragrance/poster.jpg +0 -0
- package/public/images/series/the-fallen-flowers/poster.jpg +0 -0
- package/public/images/series/the-legend-of-white-snake/poster.jpg +0 -0
- package/public/images/series/veil-of-shadows/poster.jpg +0 -0
- package/public/images/series/wanhua-sin/poster-2.jpg +0 -0
- package/public/images/series/wanhua-sin/poster.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj10yogj62u03s0npe02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj2pdvyj61li25jkjl02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj71177j62672w9e8302.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj7twpfj62192197rw02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj8jfu8j62c03401kx02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkj9dhdsj622k2l77wh02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkja31s4j62bw3407r802.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjcf331j61402ey1kx02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjdx8gqj62602pihdt02.jpg +0 -0
- package/public/images/weibo-update/41/345/210/206/351/222/237/345/211/215/0040jbadgy1igrkjfvxnzj62432n57wh02.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixniv2yj627y4fxb2b02.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixsvbdaj625k2vfu1102.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixxxdaoj62c03501l202.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3ixzeef9j62c0340npd02.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy0vs6tj626t4rbhdu02.jpg +0 -0
- package/public/images/weibo-update/8-14-17-16/0040jbadgy1ig3iy21wwuj62bt34hx6q02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly4wr85j62z629y7wh02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly67oilj623u2t5npd02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwly9qtw8j62c0340u0x02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyem4m4j63402c01kz02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlygkoq4j627s2yeavd02.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlyjh0daj62bw340nm102.jpg +0 -0
- package/public/images/weibo-update/8-8-17-39/0040jbadgy1ifwlylejlhj62bw3401kx02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaiaa8b7fj627j2y67wh02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaiagw3ezj61s02ebu0x02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaiaivng3j61rv2ebx6p02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaiajuixvj62bw340qv502.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaiapyartj62bw340hdt02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaibkwu4ij61vy2iq1kx02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaic1fvkpj61u92eo1ky02.jpg +0 -0
- package/public/images/weibo-update/today/0040jbadgy1igaic3cruhj62822ywb2902.jpg +0 -0
- package/public/manifest.json +25 -0
- package/public/mcp-logo-128.png +0 -0
- package/public/mcp-logo-64.png +0 -0
- package/public/mcp-logo.svg +16 -0
- package/server/agent/agentCore.js +542 -74
- package/server/agent/appPaths.js +1 -0
- package/server/agent/audioPlayer.js +4 -2
- package/server/agent/citations.js +294 -0
- package/server/agent/clarificationAnalytics.js +477 -0
- package/server/agent/clarifyingQuestions.js +1089 -0
- package/server/agent/cli.js +549 -125
- package/server/agent/commandFeatures.js +1 -0
- package/server/agent/commandRegistry.js +946 -879
- package/server/agent/commands.js +546 -272
- package/server/agent/config.js +581 -412
- package/server/agent/contextCompactor.js +2 -2
- package/server/agent/debate.js +235 -0
- package/server/agent/eventBus.js +3 -0
- package/server/agent/feedbackLearning.js +217 -0
- package/server/agent/llmClient.js +512 -394
- package/server/agent/mcpClient.js +182 -32
- package/server/agent/memory.js +283 -224
- package/server/agent/modelManager.js +79 -39
- package/server/agent/profiles.js +178 -0
- package/server/agent/publicTools.js +22 -0
- package/server/agent/queryDetails.js +68 -8
- package/server/agent/quizData.js +398 -384
- package/server/agent/research.js +121 -79
- package/server/agent/sanitizer.js +104 -13
- package/server/agent/session.js +319 -11
- package/server/agent/subagent.js +8 -3
- package/server/agent/suggestionTui.js +294 -0
- package/server/agent/suggestions.js +69 -3
- package/server/agent/telemetry.js +119 -0
- package/server/agent/terminalImage.js +146 -27
- package/server/agent/theme.js +17 -0
- package/server/agent/toolRouter.js +16 -1
- package/server/agent/tools.js +424 -225
- package/server/agent/ui.js +486 -64
- package/server/agent/version.js +2 -4
- package/server/agent/webSearch.js +111 -11
- package/server/agent/youtubeAuth.js +24 -6
- package/server/apiRouter.js +4222 -0
- package/server/mcp/data/discography.js +25 -16
- package/server/mcp/data/gallery.js +308 -20
- package/server/mcp/data/profile.js +74 -44
- package/server/mcp/data/timeline.js +1 -1
- package/server/mcp/data/works.js +51 -17
- package/server/mcp/dataAdapter.js +5 -3
- package/server/mcp/handler.js +5 -11
- package/server/mcp/prompts.js +86 -0
- package/server/mcp/security.js +18 -15
- package/server/mcp/server.js +7 -2
- package/server/mcp/tools.js +66 -2
- package/server/web-chat/core/browserLauncher.js +18 -0
- package/server/web-chat/core/connection.js +194 -0
- package/server/web-chat/core/conversationContext.js +82 -0
- package/server/web-chat/core/directCatalogEngine.js +196 -0
- package/server/web-chat/core/dramaKnowledge.js +142 -0
- package/server/web-chat/core/errorHandler.js +51 -0
- package/server/web-chat/core/followUpEngine.js +182 -0
- package/server/web-chat/core/futureEventGuard.js +80 -0
- package/server/web-chat/core/memorySnapshot.js +12 -0
- package/server/web-chat/core/security.js +146 -0
- package/server/web-chat/core/toolInspector.js +52 -0
- package/server/web-chat/router.js +120 -0
- package/server/web-chat/routes/chatRoute.js +397 -0
- package/server/web-chat/routes/clarifyConfigRoute.js +98 -0
- package/server/web-chat/routes/clarifyRoute.js +83 -0
- package/server/web-chat/routes/compareRoute.js +79 -0
- package/server/web-chat/routes/exportRoute.js +59 -0
- package/server/web-chat/routes/feedbackRoute.js +57 -0
- package/server/web-chat/routes/newsRoute.js +33 -0
- package/server/web-chat/routes/quizRoute.js +11 -0
- package/server/web-chat/routes/staticRoute.js +103 -0
- package/server/web-chat/routes/statusRoute.js +52 -0
- package/server/web-chat/routes/suggestRoute.js +77 -0
- package/server/web-chat/routes/timelineRoute.js +34 -0
- package/server/web-chat/routes/verifyRoute.js +57 -0
- package/server/web-chat/server.js +116 -0
- package/server/web-chat/ui/htmlBuilder.js +91 -0
- package/server/web-chat/ui/previews/agentBubble.preview.html +134 -0
- package/server/web-chat/ui/scripts/agentBubbleSystem.js +320 -0
- package/server/web-chat/ui/scripts/client.js +8397 -0
- package/server/web-chat/ui/scripts/kikuDb.js +177 -0
- package/server/web-chat/ui/scripts/suggestionsData.js +118 -0
- package/server/web-chat/ui/scripts/webchatI18n.js +291 -0
- package/server/web-chat/ui/styles/main.css +8568 -0
- package/server/web-chat/ui/templates/body.html +796 -0
- package/server/webChatHtml.js +1 -0
- package/server/webChatServer.js +6 -0
- package/src/data/discographyData.js +892 -866
- package/src/data/galleryData.js +1805 -561
- package/src/data/profileData.js +79 -2
- package/src/data/socialUpdatesData.js +106 -47
- package/src/data/timelineData.js +1 -1
- package/src/data/worksData.js +25 -4
- package/tokens.css +97 -0
- 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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
this.
|
|
14
|
-
this.
|
|
15
|
-
this.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
this.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
this.
|
|
35
|
-
this.
|
|
36
|
-
this.
|
|
37
|
-
this.
|
|
38
|
-
this.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
const controller = new AbortController();
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
(m.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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 };
|