openfox 0.1.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 (102) hide show
  1. package/README.md +68 -0
  2. package/dist/branch.api-WDHKZJS6.js +48 -0
  3. package/dist/branch.api-WDHKZJS6.js.map +1 -0
  4. package/dist/chunk-26NRL3HX.js +116 -0
  5. package/dist/chunk-26NRL3HX.js.map +1 -0
  6. package/dist/chunk-43SWNGYW.js +273 -0
  7. package/dist/chunk-43SWNGYW.js.map +1 -0
  8. package/dist/chunk-4Z2OZ6NX.js +132 -0
  9. package/dist/chunk-4Z2OZ6NX.js.map +1 -0
  10. package/dist/chunk-6MOGZMGX.js +218 -0
  11. package/dist/chunk-6MOGZMGX.js.map +1 -0
  12. package/dist/chunk-AF7XS4OI.js +55 -0
  13. package/dist/chunk-AF7XS4OI.js.map +1 -0
  14. package/dist/chunk-EI46G5WO.js +138 -0
  15. package/dist/chunk-EI46G5WO.js.map +1 -0
  16. package/dist/chunk-FF6Z2MC3.js +844 -0
  17. package/dist/chunk-FF6Z2MC3.js.map +1 -0
  18. package/dist/chunk-IKAWIOXM.js +5817 -0
  19. package/dist/chunk-IKAWIOXM.js.map +1 -0
  20. package/dist/chunk-IVHCPQO2.js +29 -0
  21. package/dist/chunk-IVHCPQO2.js.map +1 -0
  22. package/dist/chunk-MTX5Y3V3.js +46 -0
  23. package/dist/chunk-MTX5Y3V3.js.map +1 -0
  24. package/dist/chunk-N2XYX4ZA.js +83 -0
  25. package/dist/chunk-N2XYX4ZA.js.map +1 -0
  26. package/dist/chunk-QY7BMXWT.js +124 -0
  27. package/dist/chunk-QY7BMXWT.js.map +1 -0
  28. package/dist/chunk-U62ZECSQ.js +54 -0
  29. package/dist/chunk-U62ZECSQ.js.map +1 -0
  30. package/dist/chunk-YQ36AK5A.js +5570 -0
  31. package/dist/chunk-YQ36AK5A.js.map +1 -0
  32. package/dist/cli/dev.d.ts +1 -0
  33. package/dist/cli/dev.js +14 -0
  34. package/dist/cli/dev.js.map +1 -0
  35. package/dist/cli/index.d.ts +1 -0
  36. package/dist/cli/index.js +14 -0
  37. package/dist/cli/index.js.map +1 -0
  38. package/dist/command-defaults/defaults/commit-push.command.md +13 -0
  39. package/dist/command-defaults/defaults/init.command.md +18 -0
  40. package/dist/command-defaults/defaults/test-ui.command.md +16 -0
  41. package/dist/config-6GJ4SVFH.js +33 -0
  42. package/dist/config-6GJ4SVFH.js.map +1 -0
  43. package/dist/init-UVZDOHMX.js +185 -0
  44. package/dist/init-UVZDOHMX.js.map +1 -0
  45. package/dist/paths-4BPXWB2W.js +17 -0
  46. package/dist/paths-4BPXWB2W.js.map +1 -0
  47. package/dist/project-creator-CWPKRIKI.js +69 -0
  48. package/dist/project-creator-CWPKRIKI.js.map +1 -0
  49. package/dist/protocol-BJsm88OW.d.ts +760 -0
  50. package/dist/provider-LCE6WZA6.js +343 -0
  51. package/dist/provider-LCE6WZA6.js.map +1 -0
  52. package/dist/serve-27DZK34K.js +177 -0
  53. package/dist/serve-27DZK34K.js.map +1 -0
  54. package/dist/server/index.d.ts +479 -0
  55. package/dist/server/index.js +20 -0
  56. package/dist/server/index.js.map +1 -0
  57. package/dist/settings-D5O3TMLD.js +17 -0
  58. package/dist/settings-D5O3TMLD.js.map +1 -0
  59. package/dist/shared/index.d.ts +19 -0
  60. package/dist/shared/index.js +17 -0
  61. package/dist/shared/index.js.map +1 -0
  62. package/dist/skill-defaults/defaults/playwright-cli.skill.md +38 -0
  63. package/dist/tools-R4YQAYUF.js +46 -0
  64. package/dist/tools-R4YQAYUF.js.map +1 -0
  65. package/dist/web/assets/html2canvas.esm-QH1iLAAe.js +22 -0
  66. package/dist/web/assets/index-BMNDCsbJ.js +120 -0
  67. package/dist/web/assets/index-BSlSWZrM.css +1 -0
  68. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  69. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  70. package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
  71. package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
  72. package/dist/web/assets/jetbrains-mono-cyrillic-600-normal-8K4wrrwR.woff +0 -0
  73. package/dist/web/assets/jetbrains-mono-cyrillic-600-normal-EVf6-Yzo.woff2 +0 -0
  74. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  75. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  76. package/dist/web/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
  77. package/dist/web/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
  78. package/dist/web/assets/jetbrains-mono-greek-600-normal-H7WoG9Et.woff2 +0 -0
  79. package/dist/web/assets/jetbrains-mono-greek-600-normal-mc2nkWzM.woff +0 -0
  80. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  81. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  82. package/dist/web/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  83. package/dist/web/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  84. package/dist/web/assets/jetbrains-mono-latin-600-normal-BfsvjouI.woff +0 -0
  85. package/dist/web/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
  86. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  87. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  88. package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
  89. package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
  90. package/dist/web/assets/jetbrains-mono-latin-ext-600-normal-BfB_LPfz.woff2 +0 -0
  91. package/dist/web/assets/jetbrains-mono-latin-ext-600-normal-DObL3zCW.woff +0 -0
  92. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  93. package/dist/web/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
  94. package/dist/web/assets/jetbrains-mono-vietnamese-600-normal-OWROknRo.woff +0 -0
  95. package/dist/web/assets/openfox.png +0 -0
  96. package/dist/web/fox.svg +25 -0
  97. package/dist/web/index.html +14 -0
  98. package/dist/web/sounds/achievement.mp3 +0 -0
  99. package/dist/web/sounds/intervention.mp3 +0 -0
  100. package/dist/web/sounds/notification.mp3 +0 -0
  101. package/dist/web/sounds/waiting-for-user.mp3 +0 -0
  102. package/package.json +101 -0
@@ -0,0 +1,844 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-U62ZECSQ.js";
4
+
5
+ // src/server/llm/backend.ts
6
+ var BACKEND_CAPABILITIES = {
7
+ vllm: {
8
+ supportsReasoningField: true,
9
+ supportsChatTemplateKwargs: true,
10
+ supportsTopK: true
11
+ },
12
+ sglang: {
13
+ supportsReasoningField: true,
14
+ supportsChatTemplateKwargs: true,
15
+ supportsTopK: true
16
+ },
17
+ ollama: {
18
+ supportsReasoningField: false,
19
+ supportsChatTemplateKwargs: false,
20
+ supportsTopK: false
21
+ },
22
+ llamacpp: {
23
+ supportsReasoningField: false,
24
+ supportsChatTemplateKwargs: false,
25
+ supportsTopK: true
26
+ },
27
+ unknown: {
28
+ // Assume vLLM-like for unknown backends
29
+ supportsReasoningField: true,
30
+ supportsChatTemplateKwargs: true,
31
+ supportsTopK: true
32
+ }
33
+ };
34
+ function getBackendCapabilities(backend) {
35
+ return BACKEND_CAPABILITIES[backend];
36
+ }
37
+ async function detectBackend(baseUrl, explicitBackend, silent = false) {
38
+ if (explicitBackend && explicitBackend !== "unknown") {
39
+ if (silent) {
40
+ logger.debug("Using explicit backend", { backend: explicitBackend });
41
+ } else {
42
+ logger.info("Using explicit backend", { backend: explicitBackend });
43
+ }
44
+ return explicitBackend;
45
+ }
46
+ const probeUrl = baseUrl.replace(/\/v1\/?$/, "");
47
+ try {
48
+ if (await probeOllama(probeUrl)) {
49
+ if (silent) {
50
+ logger.debug("Detected Ollama backend", { url: probeUrl });
51
+ } else {
52
+ logger.info("Detected Ollama backend", { url: probeUrl });
53
+ }
54
+ return "ollama";
55
+ }
56
+ if (await probeLlamaCpp(probeUrl)) {
57
+ if (silent) {
58
+ logger.debug("Detected llama.cpp backend", { url: probeUrl });
59
+ } else {
60
+ logger.info("Detected llama.cpp backend", { url: probeUrl });
61
+ }
62
+ return "llamacpp";
63
+ }
64
+ if (await probeSGLang(probeUrl)) {
65
+ if (silent) {
66
+ logger.debug("Detected SGLang backend", { url: probeUrl });
67
+ } else {
68
+ logger.info("Detected SGLang backend", { url: probeUrl });
69
+ }
70
+ return "sglang";
71
+ }
72
+ if (await probeOpenAI(baseUrl)) {
73
+ if (silent) {
74
+ logger.debug("Detected vLLM backend (OpenAI-compatible)", { url: baseUrl });
75
+ } else {
76
+ logger.info("Detected vLLM backend (OpenAI-compatible)", { url: baseUrl });
77
+ }
78
+ return "vllm";
79
+ }
80
+ if (silent) {
81
+ logger.debug("Could not detect backend, using unknown", { url: baseUrl });
82
+ } else {
83
+ logger.warn("Could not detect backend, using unknown", { url: baseUrl });
84
+ }
85
+ return "unknown";
86
+ } catch (error) {
87
+ if (silent) {
88
+ logger.debug("Backend detection failed", {
89
+ url: baseUrl,
90
+ error: error instanceof Error ? error.message : String(error)
91
+ });
92
+ } else {
93
+ logger.warn("Backend detection failed", {
94
+ url: baseUrl,
95
+ error: error instanceof Error ? error.message : String(error)
96
+ });
97
+ }
98
+ return "unknown";
99
+ }
100
+ }
101
+ async function probeOllama(baseUrl) {
102
+ try {
103
+ const response = await fetch(`${baseUrl}/api/tags`, {
104
+ signal: AbortSignal.timeout(5e3)
105
+ });
106
+ return response.ok;
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+ async function probeLlamaCpp(baseUrl) {
112
+ try {
113
+ const response = await fetch(`${baseUrl}/health`, {
114
+ signal: AbortSignal.timeout(5e3)
115
+ });
116
+ if (!response.ok) return false;
117
+ const data = await response.json();
118
+ return "slots_idle" in data || "slots_processing" in data;
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+ async function probeSGLang(baseUrl) {
124
+ try {
125
+ const response = await fetch(`${baseUrl}/get_model_info`, {
126
+ signal: AbortSignal.timeout(5e3)
127
+ });
128
+ return response.ok;
129
+ } catch {
130
+ return false;
131
+ }
132
+ }
133
+ async function probeOpenAI(baseUrl) {
134
+ try {
135
+ const url = baseUrl.includes("/v1") ? baseUrl : `${baseUrl}/v1`;
136
+ const response = await fetch(`${url}/models`, {
137
+ signal: AbortSignal.timeout(5e3)
138
+ });
139
+ return response.ok;
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+ function getBackendDisplayName(backend) {
145
+ switch (backend) {
146
+ case "vllm":
147
+ return "vLLM";
148
+ case "sglang":
149
+ return "SGLang";
150
+ case "ollama":
151
+ return "Ollama";
152
+ case "llamacpp":
153
+ return "llama.cpp";
154
+ case "unknown":
155
+ return "Unknown";
156
+ }
157
+ }
158
+
159
+ // src/server/llm/models.ts
160
+ var modelCache = /* @__PURE__ */ new Map();
161
+ var llmStatus = "unknown";
162
+ var lastActiveUrl = null;
163
+ var CACHE_TTL_MS = 3e4;
164
+ function getCacheKey(url) {
165
+ return url.replace(/\/v1\/?$/, "");
166
+ }
167
+ async function detectModel(llmBaseUrl, retries = 3, silent = false) {
168
+ const cacheKey = getCacheKey(llmBaseUrl);
169
+ const now = Date.now();
170
+ const cached = modelCache.get(cacheKey);
171
+ if (cached && now - cached.timestamp < CACHE_TTL_MS) {
172
+ lastActiveUrl = cacheKey;
173
+ llmStatus = "connected";
174
+ return cached.model;
175
+ }
176
+ const url = llmBaseUrl.includes("/v1") ? `${llmBaseUrl}/models` : `${llmBaseUrl}/v1/models`;
177
+ for (let attempt = 1; attempt <= retries; attempt++) {
178
+ try {
179
+ if (silent) {
180
+ logger.debug("Fetching models from LLM server", { url, attempt });
181
+ }
182
+ const response = await fetch(url, {
183
+ signal: AbortSignal.timeout(1e4)
184
+ });
185
+ if (!response.ok) {
186
+ if (silent) {
187
+ logger.debug("Failed to fetch models from LLM server", { status: response.status, attempt });
188
+ } else {
189
+ logger.warn("Failed to fetch models from LLM server", { status: response.status, attempt });
190
+ }
191
+ if (attempt < retries) {
192
+ await new Promise((r) => setTimeout(r, 1e3 * attempt));
193
+ continue;
194
+ }
195
+ llmStatus = "disconnected";
196
+ return cached?.model ?? null;
197
+ }
198
+ const data = await response.json();
199
+ if (data.data && data.data.length > 0) {
200
+ const modelData = data.data[0];
201
+ const modelId = modelData.id;
202
+ modelCache.set(cacheKey, {
203
+ model: modelId,
204
+ modelInfo: modelData,
205
+ timestamp: now
206
+ });
207
+ lastActiveUrl = cacheKey;
208
+ llmStatus = "connected";
209
+ if (silent) {
210
+ logger.debug("Detected LLM model", {
211
+ model: modelId,
212
+ maxLen: modelData.max_model_len,
213
+ root: modelData.root
214
+ });
215
+ } else {
216
+ logger.info("Detected LLM model", {
217
+ model: modelId,
218
+ maxLen: modelData.max_model_len,
219
+ root: modelData.root
220
+ });
221
+ }
222
+ return modelId;
223
+ }
224
+ if (silent) {
225
+ logger.debug("LLM server returned empty models list");
226
+ } else {
227
+ logger.warn("LLM server returned empty models list");
228
+ }
229
+ llmStatus = "disconnected";
230
+ return null;
231
+ } catch (error) {
232
+ const errMsg = error instanceof Error ? error.message : String(error);
233
+ if (silent) {
234
+ logger.debug("Could not detect model from LLM server", { error: errMsg, attempt });
235
+ } else {
236
+ logger.warn("Could not detect model from LLM server", { error: errMsg, attempt });
237
+ }
238
+ if (attempt < retries) {
239
+ await new Promise((r) => setTimeout(r, 1e3 * attempt));
240
+ continue;
241
+ }
242
+ }
243
+ }
244
+ llmStatus = "disconnected";
245
+ return cached?.model ?? null;
246
+ }
247
+ function getLlmStatus() {
248
+ return llmStatus;
249
+ }
250
+ function clearModelCache(url) {
251
+ if (url) {
252
+ modelCache.delete(getCacheKey(url));
253
+ } else {
254
+ modelCache.clear();
255
+ }
256
+ llmStatus = "unknown";
257
+ }
258
+
259
+ // src/server/llm/client.ts
260
+ import OpenAI from "openai";
261
+
262
+ // src/server/utils/errors.ts
263
+ var OpenFoxError = class extends Error {
264
+ constructor(message, code, details) {
265
+ super(message);
266
+ this.code = code;
267
+ this.details = details;
268
+ this.name = "OpenFoxError";
269
+ }
270
+ };
271
+ var SessionNotFoundError = class extends OpenFoxError {
272
+ constructor(sessionId) {
273
+ super(`Session not found: ${sessionId}`, "SESSION_NOT_FOUND", { sessionId });
274
+ this.name = "SessionNotFoundError";
275
+ }
276
+ };
277
+ var LLMError = class extends OpenFoxError {
278
+ constructor(message, details) {
279
+ super(message, "LLM_ERROR", details);
280
+ this.name = "LLMError";
281
+ }
282
+ };
283
+
284
+ // src/server/llm/profiles.ts
285
+ var DEFAULT_PROFILE = {
286
+ name: "default",
287
+ temperature: 0.7,
288
+ topP: 0.9,
289
+ supportsReasoning: false,
290
+ reasoningAsContent: false,
291
+ defaultMaxTokens: 4096
292
+ };
293
+ var MOCK_PROFILE = {
294
+ name: "mock",
295
+ temperature: 0.7,
296
+ topP: 0.9,
297
+ supportsReasoning: false,
298
+ reasoningAsContent: false,
299
+ defaultMaxTokens: 1024
300
+ };
301
+ var MODEL_PROFILES = [
302
+ {
303
+ pattern: "mistral",
304
+ profile: {
305
+ name: "Mistral",
306
+ temperature: 0.7,
307
+ topP: 0.9,
308
+ supportsReasoning: false,
309
+ reasoningAsContent: false,
310
+ defaultMaxTokens: 4096
311
+ }
312
+ },
313
+ {
314
+ pattern: "qwen3-coder-next",
315
+ profile: {
316
+ name: "Qwen3-Coder-Next",
317
+ // Per Qwen docs: "temperature=1.0, top_p=0.95, top_k=40"
318
+ temperature: 1,
319
+ topP: 0.95,
320
+ topK: 40,
321
+ // "This model supports only non-thinking mode and does not generate <think></think> blocks"
322
+ supportsReasoning: false,
323
+ reasoningAsContent: false,
324
+ defaultMaxTokens: 8192
325
+ }
326
+ },
327
+ {
328
+ pattern: "qwen3",
329
+ profile: {
330
+ name: "Qwen3",
331
+ temperature: 0.7,
332
+ topP: 0.9,
333
+ supportsReasoning: true,
334
+ reasoningAsContent: false,
335
+ defaultMaxTokens: 4096
336
+ }
337
+ },
338
+ {
339
+ pattern: "deepseek",
340
+ profile: {
341
+ name: "DeepSeek",
342
+ temperature: 0.6,
343
+ topP: 0.95,
344
+ supportsReasoning: true,
345
+ reasoningAsContent: false,
346
+ defaultMaxTokens: 4096
347
+ }
348
+ },
349
+ {
350
+ pattern: "llama",
351
+ profile: {
352
+ name: "Llama",
353
+ temperature: 0.7,
354
+ topP: 0.9,
355
+ supportsReasoning: false,
356
+ reasoningAsContent: false,
357
+ defaultMaxTokens: 4096
358
+ }
359
+ },
360
+ {
361
+ pattern: "claude",
362
+ profile: {
363
+ name: "Claude",
364
+ temperature: 0.7,
365
+ topP: 0.9,
366
+ supportsReasoning: true,
367
+ reasoningAsContent: false,
368
+ defaultMaxTokens: 4096
369
+ }
370
+ }
371
+ ];
372
+ function getModelProfile(modelName) {
373
+ const lowerName = modelName.toLowerCase();
374
+ if (lowerName.includes("mock")) {
375
+ return MOCK_PROFILE;
376
+ }
377
+ for (const { pattern, profile } of MODEL_PROFILES) {
378
+ if (lowerName.includes(pattern.toLowerCase())) {
379
+ return profile;
380
+ }
381
+ }
382
+ return DEFAULT_PROFILE;
383
+ }
384
+
385
+ // src/server/llm/client-pure.ts
386
+ function convertMessages(messages) {
387
+ const filtered = messages.filter((msg) => {
388
+ return !(msg.role === "assistant" && !msg.content?.trim() && !msg.toolCalls?.length);
389
+ });
390
+ return filtered.map((msg) => {
391
+ if (msg.role === "tool") {
392
+ return {
393
+ role: "tool",
394
+ content: msg.content,
395
+ tool_call_id: msg.toolCallId
396
+ };
397
+ }
398
+ if (msg.role === "assistant" && msg.toolCalls?.length) {
399
+ return {
400
+ role: "assistant",
401
+ content: msg.content || null,
402
+ tool_calls: msg.toolCalls.map((toolCall) => ({
403
+ id: toolCall.id,
404
+ type: "function",
405
+ function: {
406
+ name: toolCall.name,
407
+ arguments: JSON.stringify(toolCall.arguments)
408
+ }
409
+ }))
410
+ };
411
+ }
412
+ if (msg.role === "user" && msg.attachments && msg.attachments.length > 0) {
413
+ const content = [];
414
+ if (msg.content?.trim()) {
415
+ content.push({ type: "text", text: msg.content });
416
+ }
417
+ for (const attachment of msg.attachments) {
418
+ content.push({
419
+ type: "image_url",
420
+ image_url: {
421
+ url: attachment.data
422
+ // base64 data URL
423
+ }
424
+ });
425
+ }
426
+ return {
427
+ role: "user",
428
+ content
429
+ };
430
+ }
431
+ return {
432
+ role: msg.role,
433
+ content: msg.content
434
+ };
435
+ });
436
+ }
437
+ function convertTools(tools) {
438
+ return tools.map((tool) => ({
439
+ type: "function",
440
+ function: {
441
+ name: tool.function.name,
442
+ description: tool.function.description,
443
+ parameters: tool.function.parameters
444
+ }
445
+ }));
446
+ }
447
+ function buildNonStreamingCreateParams(input) {
448
+ const { model, request, profile, capabilities, disableThinking } = input;
449
+ const params = {
450
+ model,
451
+ messages: convertMessages(request.messages),
452
+ ...request.tools ? { tools: convertTools(request.tools) } : {},
453
+ ...request.toolChoice ? { tool_choice: request.toolChoice } : {},
454
+ temperature: request.temperature ?? profile.temperature,
455
+ max_tokens: request.maxTokens ?? profile.defaultMaxTokens,
456
+ top_p: profile.topP,
457
+ stream: false
458
+ };
459
+ if (capabilities.supportsTopK && profile.topK !== void 0) {
460
+ ;
461
+ params["top_k"] = profile.topK;
462
+ }
463
+ if (capabilities.supportsChatTemplateKwargs && profile.supportsReasoning && disableThinking) {
464
+ ;
465
+ params["chat_template_kwargs"] = { enable_thinking: false };
466
+ }
467
+ return params;
468
+ }
469
+ function buildStreamingCreateParams(input) {
470
+ const { model, request, profile, capabilities, disableThinking } = input;
471
+ const params = {
472
+ model,
473
+ messages: convertMessages(request.messages),
474
+ ...request.tools ? { tools: convertTools(request.tools) } : {},
475
+ ...request.toolChoice ? { tool_choice: request.toolChoice } : {},
476
+ temperature: request.temperature ?? profile.temperature,
477
+ max_tokens: request.maxTokens ?? profile.defaultMaxTokens,
478
+ top_p: profile.topP,
479
+ stream: true,
480
+ stream_options: { include_usage: true }
481
+ };
482
+ if (capabilities.supportsTopK && profile.topK !== void 0) {
483
+ ;
484
+ params["top_k"] = profile.topK;
485
+ }
486
+ if (capabilities.supportsChatTemplateKwargs && profile.supportsReasoning && (disableThinking || request.disableThinking)) {
487
+ ;
488
+ params["chat_template_kwargs"] = { enable_thinking: false };
489
+ }
490
+ return params;
491
+ }
492
+ function mapFinishReason(reason) {
493
+ switch (reason) {
494
+ case "stop":
495
+ return "stop";
496
+ case "tool_calls":
497
+ return "tool_calls";
498
+ case "length":
499
+ return "length";
500
+ case "content_filter":
501
+ return "content_filter";
502
+ default:
503
+ return "stop";
504
+ }
505
+ }
506
+ function extractThinking(content) {
507
+ const thinkRegex = /<think>([\s\S]*?)<\/think>/g;
508
+ let thinkingContent = "";
509
+ let cleanContent = content;
510
+ let match;
511
+ while ((match = thinkRegex.exec(content)) !== null) {
512
+ thinkingContent += match[1];
513
+ cleanContent = cleanContent.replace(match[0], "");
514
+ }
515
+ return {
516
+ content: cleanContent.trim(),
517
+ thinkingContent: thinkingContent.trim() || null
518
+ };
519
+ }
520
+
521
+ // src/server/llm/client.ts
522
+ function createLLMClient(config, initialBackend = "unknown") {
523
+ const baseURL = config.llm.baseUrl.includes("/v1") ? config.llm.baseUrl : `${config.llm.baseUrl}/v1`;
524
+ const openai = new OpenAI({
525
+ baseURL,
526
+ apiKey: "not-needed"
527
+ // Most local backends don't require API key
528
+ });
529
+ let model = config.llm.model;
530
+ let profile = getModelProfile(model);
531
+ let backend = initialBackend;
532
+ let capabilities = getBackendCapabilities(backend);
533
+ const disableThinking = config.llm.disableThinking ?? false;
534
+ const idleTimeout = config.llm.idleTimeout ?? 3e4;
535
+ return {
536
+ getModel() {
537
+ return model;
538
+ },
539
+ getProfile() {
540
+ return profile;
541
+ },
542
+ getBackend() {
543
+ return backend;
544
+ },
545
+ setBackend(newBackend) {
546
+ logger.debug("Setting LLM backend", { from: backend, to: newBackend });
547
+ backend = newBackend;
548
+ capabilities = getBackendCapabilities(newBackend);
549
+ },
550
+ setModel(newModel) {
551
+ const newProfile = getModelProfile(newModel);
552
+ logger.debug("Switching model", {
553
+ from: model,
554
+ to: newModel,
555
+ profile: newProfile.name,
556
+ temperature: newProfile.temperature,
557
+ supportsReasoning: newProfile.supportsReasoning
558
+ });
559
+ model = newModel;
560
+ profile = newProfile;
561
+ },
562
+ async complete(request) {
563
+ logger.debug("LLM complete request", {
564
+ messageCount: request.messages.length,
565
+ hasTools: !!request.tools?.length,
566
+ profile: profile.name,
567
+ disableThinking
568
+ });
569
+ try {
570
+ const shouldDisableThinking = disableThinking || request.disableThinking === true;
571
+ const createParams = buildNonStreamingCreateParams({
572
+ model,
573
+ request,
574
+ profile,
575
+ capabilities,
576
+ disableThinking: shouldDisableThinking
577
+ });
578
+ const response = await openai.chat.completions.create(createParams, {
579
+ signal: request.signal
580
+ });
581
+ const choice = response.choices[0];
582
+ if (!choice) {
583
+ throw new LLMError("No completion choice returned");
584
+ }
585
+ const message = choice.message;
586
+ let content = message.content ?? "";
587
+ let thinkingContent = "";
588
+ if (profile.supportsReasoning) {
589
+ if (capabilities.supportsReasoningField) {
590
+ thinkingContent = message.reasoning_content ?? message.reasoning ?? "";
591
+ } else {
592
+ const extracted = extractThinking(content);
593
+ content = extracted.content;
594
+ thinkingContent = extracted.thinkingContent ?? "";
595
+ }
596
+ if (profile.reasoningAsContent && thinkingContent.trim()) {
597
+ content = thinkingContent;
598
+ thinkingContent = "";
599
+ }
600
+ }
601
+ if (!content.trim() && thinkingContent.trim()) {
602
+ content = thinkingContent;
603
+ thinkingContent = "";
604
+ }
605
+ const toolCalls = message.tool_calls?.map((tc) => ({
606
+ id: tc.id,
607
+ name: tc.function.name,
608
+ arguments: JSON.parse(tc.function.arguments)
609
+ }));
610
+ return {
611
+ id: response.id,
612
+ content,
613
+ ...thinkingContent ? { thinkingContent } : {},
614
+ ...toolCalls && toolCalls.length > 0 ? { toolCalls } : {},
615
+ finishReason: mapFinishReason(choice.finish_reason),
616
+ usage: {
617
+ promptTokens: response.usage?.prompt_tokens ?? 0,
618
+ completionTokens: response.usage?.completion_tokens ?? 0,
619
+ totalTokens: response.usage?.total_tokens ?? 0
620
+ }
621
+ };
622
+ } catch (error) {
623
+ logger.error("LLM complete error", { error: error.toString() });
624
+ throw new LLMError(
625
+ error instanceof Error ? error.message : "Unknown LLM error",
626
+ { originalError: error }
627
+ );
628
+ }
629
+ },
630
+ async *stream(request) {
631
+ logger.debug("LLM stream request", {
632
+ messageCount: request.messages.length,
633
+ hasTools: !!request.tools?.length,
634
+ profile: profile.name,
635
+ disableThinking,
636
+ idleTimeout
637
+ });
638
+ try {
639
+ const shouldDisableThinking = disableThinking || request.disableThinking === true;
640
+ const createParams = buildStreamingCreateParams({ model, request, profile, capabilities, disableThinking: shouldDisableThinking });
641
+ const stream = await openai.chat.completions.create(createParams, {
642
+ signal: request.signal
643
+ });
644
+ let fullContent = "";
645
+ let fullThinking = "";
646
+ let inThinking = false;
647
+ let tagBuffer = "";
648
+ const toolCalls = /* @__PURE__ */ new Map();
649
+ let finishReason = "stop";
650
+ let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
651
+ let responseId = "";
652
+ let lastChunkTime = Date.now();
653
+ const idleTimeoutController = new AbortController();
654
+ const idleTimer = setInterval(() => {
655
+ const idleDuration = Date.now() - lastChunkTime;
656
+ if (idleDuration > idleTimeout) {
657
+ logger.warn("LLM stream idle timeout triggered", { idleDuration, idleTimeout });
658
+ idleTimeoutController.abort();
659
+ }
660
+ }, 100);
661
+ try {
662
+ for await (const chunk of stream) {
663
+ if (idleTimeoutController.signal.aborted) {
664
+ throw new Error(`LLM stream idle timeout: no chunks received for ${idleTimeout}ms`);
665
+ }
666
+ lastChunkTime = Date.now();
667
+ responseId = chunk.id;
668
+ if (chunk.usage) {
669
+ usage = {
670
+ promptTokens: chunk.usage.prompt_tokens,
671
+ completionTokens: chunk.usage.completion_tokens,
672
+ totalTokens: chunk.usage.total_tokens
673
+ };
674
+ }
675
+ const choice = chunk.choices[0];
676
+ if (!choice) continue;
677
+ if (choice.finish_reason) {
678
+ finishReason = mapFinishReason(choice.finish_reason);
679
+ }
680
+ const delta = choice.delta;
681
+ if (capabilities.supportsReasoningField) {
682
+ const reasoning = delta.reasoning_content ?? delta.reasoning;
683
+ if (reasoning) {
684
+ if (profile.supportsReasoning && !profile.reasoningAsContent) {
685
+ fullThinking += reasoning;
686
+ yield { type: "thinking_delta", content: reasoning };
687
+ } else {
688
+ fullContent += reasoning;
689
+ yield { type: "text_delta", content: reasoning };
690
+ }
691
+ }
692
+ }
693
+ if (delta.content) {
694
+ fullContent += delta.content;
695
+ if (!capabilities.supportsReasoningField && profile.supportsReasoning && !profile.reasoningAsContent) {
696
+ tagBuffer += delta.content;
697
+ while (tagBuffer.length > 0) {
698
+ if (inThinking) {
699
+ const closeIdx = tagBuffer.indexOf("</think>");
700
+ if (closeIdx !== -1) {
701
+ const thinkText = tagBuffer.slice(0, closeIdx);
702
+ if (thinkText) {
703
+ fullThinking += thinkText;
704
+ yield { type: "thinking_delta", content: thinkText };
705
+ }
706
+ tagBuffer = tagBuffer.slice(closeIdx + "</think>".length);
707
+ inThinking = false;
708
+ } else if (tagBuffer.includes("</")) {
709
+ const partialIdx = tagBuffer.lastIndexOf("</");
710
+ const before = tagBuffer.slice(0, partialIdx);
711
+ if (before) {
712
+ fullThinking += before;
713
+ yield { type: "thinking_delta", content: before };
714
+ }
715
+ tagBuffer = tagBuffer.slice(partialIdx);
716
+ break;
717
+ } else {
718
+ fullThinking += tagBuffer;
719
+ yield { type: "thinking_delta", content: tagBuffer };
720
+ tagBuffer = "";
721
+ }
722
+ } else {
723
+ const openIdx = tagBuffer.indexOf("<think>");
724
+ if (openIdx !== -1) {
725
+ const textBefore = tagBuffer.slice(0, openIdx);
726
+ if (textBefore) {
727
+ yield { type: "text_delta", content: textBefore };
728
+ }
729
+ tagBuffer = tagBuffer.slice(openIdx + "<think>".length);
730
+ inThinking = true;
731
+ } else if (tagBuffer.includes("<")) {
732
+ const partialIdx = tagBuffer.lastIndexOf("<");
733
+ const before = tagBuffer.slice(0, partialIdx);
734
+ if (before) {
735
+ yield { type: "text_delta", content: before };
736
+ }
737
+ tagBuffer = tagBuffer.slice(partialIdx);
738
+ break;
739
+ } else {
740
+ yield { type: "text_delta", content: tagBuffer };
741
+ tagBuffer = "";
742
+ }
743
+ }
744
+ }
745
+ } else {
746
+ yield { type: "text_delta", content: delta.content };
747
+ }
748
+ }
749
+ if (delta.tool_calls) {
750
+ for (const tc of delta.tool_calls) {
751
+ const existing = toolCalls.get(tc.index);
752
+ if (!existing) {
753
+ toolCalls.set(tc.index, {
754
+ id: tc.id ?? "",
755
+ name: tc.function?.name ?? "",
756
+ arguments: tc.function?.arguments ?? ""
757
+ });
758
+ } else {
759
+ if (tc.id) existing.id = tc.id;
760
+ if (tc.function?.name) existing.name += tc.function.name;
761
+ if (tc.function?.arguments) existing.arguments += tc.function.arguments;
762
+ }
763
+ yield {
764
+ type: "tool_call_delta",
765
+ index: tc.index,
766
+ ...tc.id ? { id: tc.id } : {},
767
+ ...tc.function?.name ? { name: tc.function.name } : {},
768
+ ...tc.function?.arguments ? { arguments: tc.function.arguments } : {}
769
+ };
770
+ }
771
+ }
772
+ }
773
+ } finally {
774
+ clearInterval(idleTimer);
775
+ }
776
+ if (tagBuffer) {
777
+ if (inThinking) {
778
+ fullThinking += tagBuffer;
779
+ }
780
+ tagBuffer = "";
781
+ }
782
+ let finalContent = fullContent.trim();
783
+ let finalThinking = fullThinking.trim();
784
+ if (!capabilities.supportsReasoningField && profile.supportsReasoning) {
785
+ const extracted = extractThinking(finalContent);
786
+ finalContent = extracted.content;
787
+ finalThinking = extracted.thinkingContent ?? "";
788
+ }
789
+ if (!finalContent && finalThinking) {
790
+ finalContent = finalThinking;
791
+ finalThinking = "";
792
+ }
793
+ const parsedToolCalls = [];
794
+ for (const [, tc] of toolCalls) {
795
+ try {
796
+ parsedToolCalls.push({
797
+ id: tc.id,
798
+ name: tc.name,
799
+ arguments: JSON.parse(tc.arguments)
800
+ });
801
+ } catch (error) {
802
+ logger.warn("Failed to parse tool call arguments", { name: tc.name, arguments: tc.arguments });
803
+ parsedToolCalls.push({
804
+ id: tc.id,
805
+ name: tc.name,
806
+ arguments: {},
807
+ parseError: error instanceof Error ? error.message : "Unknown JSON parse error",
808
+ rawArguments: tc.arguments
809
+ });
810
+ }
811
+ }
812
+ yield {
813
+ type: "done",
814
+ response: {
815
+ id: responseId,
816
+ content: finalContent,
817
+ ...finalThinking ? { thinkingContent: finalThinking } : {},
818
+ ...parsedToolCalls.length > 0 ? { toolCalls: parsedToolCalls } : {},
819
+ finishReason,
820
+ usage
821
+ }
822
+ };
823
+ } catch (error) {
824
+ logger.error("LLM stream error", { error });
825
+ yield {
826
+ type: "error",
827
+ error: error instanceof Error ? error.message : "Unknown LLM error"
828
+ };
829
+ }
830
+ }
831
+ };
832
+ }
833
+
834
+ export {
835
+ SessionNotFoundError,
836
+ getModelProfile,
837
+ detectBackend,
838
+ getBackendDisplayName,
839
+ createLLMClient,
840
+ detectModel,
841
+ getLlmStatus,
842
+ clearModelCache
843
+ };
844
+ //# sourceMappingURL=chunk-FF6Z2MC3.js.map