kiku-agent 1.0.10 → 1.0.11
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 +1 -1
- package/package.json +1 -1
- package/server/agent/agentCore.js +5 -8
- package/server/agent/appPaths.js +2 -0
- package/server/agent/cli.js +457 -248
- package/server/agent/commandRegistry.js +367 -40
- package/server/agent/commands.js +145 -41
- package/server/agent/config.js +367 -216
- package/server/agent/doctor.js +1 -1
- package/server/agent/errors.js +205 -0
- package/server/agent/llmClient.js +369 -351
- package/server/agent/queryDetails.js +234 -182
- package/server/agent/session.js +410 -258
- package/server/agent/terminalImage.js +108 -2
- package/server/agent/ui.js +17 -11
- package/src/assets/images/0040jbadgy1igaiagw3ezj61s02ebu0x02.jpg +0 -0
- package/src/assets/images/0040jbadgy1igaiaivng3j61rv2ebx6p02.jpg +0 -0
- package/src/assets/images/0040jbadgy1igaiajuixvj62bw340qv502.jpg +0 -0
- package/src/assets/images/0040jbadgy1igdxrcipx4j61k422wtpp02.jpg +0 -0
- package/src/assets/images/0040jbadgy1igdxrg3cq5j62bw340u0x02.jpg +0 -0
- package/src/assets/images/0040jbadgy1igdy9hpg8aj62c0340hdt02.jpg +0 -0
|
@@ -1,209 +1,215 @@
|
|
|
1
|
-
import { config, CLOUD_PROVIDERS, normalizeProviderModel } from './config.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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 = {}) {
|
|
5
6
|
const providerId = (options.provider || config.activeProvider || 'lmstudio').toLowerCase();
|
|
6
7
|
const providerDef = CLOUD_PROVIDERS[providerId];
|
|
7
8
|
if (!providerDef) {
|
|
8
9
|
throw new Error(`Unknown provider "${providerId}". Supported: ${Object.keys(CLOUD_PROVIDERS).join(', ')}`);
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
-
this.provider = providerId;
|
|
12
|
-
this.providerName = providerDef.name || providerId;
|
|
13
|
-
this.baseUrl = (options.baseUrl || providerDef.baseUrl || config.lmStudio.baseUrl).replace(/\/+$/, '');
|
|
14
|
-
this.apiKey = options.apiKey !== undefined
|
|
15
|
-
? options.apiKey
|
|
16
|
-
: (providerDef.apiKey !== undefined ? providerDef.apiKey : (providerDef.isLocal ? (config.lmStudio.apiKey || 'lm-studio') : ''));
|
|
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') : ''));
|
|
17
18
|
this.model = normalizeProviderModel(
|
|
18
19
|
providerId,
|
|
19
20
|
options.model || providerDef.defaultModel || config.lmStudio.model || 'google/gemma-4-e4b'
|
|
20
21
|
);
|
|
21
|
-
this.timeout = options.timeout || config.lmStudio.requestTimeoutMs || 180000;
|
|
22
|
-
this.extraHeaders = { ...(providerDef.extraHeaders || {}), ...(options.extraHeaders || {}) };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
switchProvider(providerId, options = {}) {
|
|
26
|
-
const pId = String(providerId || '').toLowerCase().trim();
|
|
27
|
-
const providerDef = CLOUD_PROVIDERS[pId];
|
|
28
|
-
if (!providerDef) {
|
|
29
|
-
throw new Error(`Unknown provider "${providerId}". Supported: ${Object.keys(CLOUD_PROVIDERS).join(', ')}`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
this.provider = pId;
|
|
33
|
-
this.providerName = providerDef.name || pId;
|
|
34
|
-
this.baseUrl = (options.baseUrl || providerDef.baseUrl).replace(/\/+$/, '');
|
|
35
|
-
this.apiKey = options.apiKey !== undefined ? options.apiKey : (providerDef.apiKey || '');
|
|
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 || '');
|
|
36
37
|
this.model = normalizeProviderModel(pId, options.model || providerDef.defaultModel);
|
|
37
|
-
this.extraHeaders = { ...(providerDef.extraHeaders || {}), ...(options.extraHeaders || {}) };
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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.');
|
|
49
57
|
this.model = normalizeProviderModel(this.provider, trimmed);
|
|
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
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (res.status ===
|
|
133
|
-
formattedError = `
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
m
|
|
154
|
-
m
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
onThought = null
|
|
195
|
-
}) {
|
|
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
|
+
}) {
|
|
196
202
|
const isLegacyReasoningModel = /^(o1|o3|deepseek-reasoner)/i.test(this.model) || this.model.includes('reasoner');
|
|
197
203
|
const usesCompletionTokenLimit = /^(o1|o3|gpt-5(?:\.|-|$))/i.test(this.model);
|
|
198
204
|
const rejectsTemperature = /^(o1|o3|gpt-5(?:\.|-|$))/i.test(this.model);
|
|
199
|
-
|
|
200
|
-
const buildPayload = (includeTools = true, includeTemp = true) => {
|
|
201
|
-
const p = {
|
|
202
|
-
model: this.model,
|
|
203
|
-
messages,
|
|
204
|
-
stream
|
|
205
|
-
};
|
|
206
|
-
|
|
205
|
+
|
|
206
|
+
const buildPayload = (includeTools = true, includeTemp = true) => {
|
|
207
|
+
const p = {
|
|
208
|
+
model: this.model,
|
|
209
|
+
messages,
|
|
210
|
+
stream
|
|
211
|
+
};
|
|
212
|
+
|
|
207
213
|
if (!rejectsTemperature && includeTemp && temperature !== undefined && temperature !== null) {
|
|
208
214
|
p.temperature = temperature;
|
|
209
215
|
}
|
|
@@ -215,162 +221,174 @@ export class LLMClient {
|
|
|
215
221
|
}
|
|
216
222
|
|
|
217
223
|
if (includeTools && tools && Array.isArray(tools) && tools.length > 0 && !isLegacyReasoningModel) {
|
|
218
|
-
p.tools = tools;
|
|
219
|
-
p.tool_choice = "auto";
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return p;
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
let payload = buildPayload(true, true);
|
|
226
|
-
|
|
227
|
-
const controller = new AbortController();
|
|
228
|
-
let timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
229
|
-
|
|
230
|
-
const abortHandler = () => controller.abort();
|
|
231
|
-
if (signal) signal.addEventListener('abort', abortHandler, { once: true });
|
|
232
|
-
|
|
233
|
-
const resetTimeout = () => {
|
|
234
|
-
clearTimeout(timeoutId);
|
|
235
|
-
timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
let res = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
240
|
-
method: 'POST',
|
|
241
|
-
headers: this._buildHeaders(),
|
|
242
|
-
body: JSON.stringify(payload),
|
|
243
|
-
signal: controller.signal
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
resetTimeout();
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (!res.ok && res.status === 400) {
|
|
250
|
-
const errText = await res.text();
|
|
251
|
-
const lowerErr = errText.toLowerCase();
|
|
252
|
-
let retryNeeded = false;
|
|
253
|
-
|
|
254
|
-
if ((lowerErr.includes('tool') || lowerErr.includes('function') || lowerErr.includes('schema')) && payload.tools) {
|
|
255
|
-
payload = buildPayload(false, true);
|
|
256
|
-
retryNeeded = true;
|
|
257
|
-
} else if (lowerErr.includes('temperature') && payload.temperature !== undefined) {
|
|
258
|
-
payload = buildPayload(Boolean(payload.tools), false);
|
|
259
|
-
retryNeeded = true;
|
|
260
|
-
} else if (lowerErr.includes('max_tokens') && payload.max_tokens !== undefined) {
|
|
261
|
-
delete payload.max_tokens;
|
|
262
|
-
payload.max_completion_tokens = maxTokens;
|
|
263
|
-
retryNeeded = true;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (retryNeeded) {
|
|
267
|
-
res = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
268
|
-
method: 'POST',
|
|
269
|
-
headers: this._buildHeaders(),
|
|
270
|
-
body: JSON.stringify(payload),
|
|
271
|
-
signal: controller.signal
|
|
272
|
-
});
|
|
273
|
-
resetTimeout();
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (!res.ok) {
|
|
277
|
-
const finalErr = retryNeeded ? await res.text() : errText;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const
|
|
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
|
-
|
|
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 };
|