nyxora 26.6.25 → 26.6.27

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 (69) hide show
  1. package/dist/packages/core/src/agent/llmProvider.js +13 -4
  2. package/dist/packages/core/src/agent/osAgent.js +15 -83
  3. package/dist/packages/core/src/agent/reasoning.js +15 -115
  4. package/dist/packages/core/src/agent/web3Agent.js +15 -81
  5. package/dist/packages/core/src/config/parser.js +14 -1
  6. package/dist/packages/core/src/gateway/server.js +4 -1
  7. package/dist/packages/core/src/gateway/telegram.js +5 -5
  8. package/dist/packages/core/src/memory/logger.js +17 -6
  9. package/dist/packages/core/src/memory/reflection.js +2 -2
  10. package/dist/packages/core/src/system/skills/audioTranscribe.js +2 -2
  11. package/dist/packages/core/src/system/skills/summarizeText.js +2 -2
  12. package/dist/packages/core/src/utils/historySanitizer.js +27 -17
  13. package/dist/packages/core/src/utils/httpClient.js +22 -5
  14. package/dist/packages/core/src/utils/llmUtils.js +126 -0
  15. package/dist/packages/core/src/web3/aggregator/defiRouter.js +4 -2
  16. package/dist/packages/core/src/web3/aggregator/providerRegistry.js +8 -1
  17. package/dist/packages/core/src/web3/aggregator/providers/KyberSwapProvider.js +8 -4
  18. package/dist/packages/core/src/web3/aggregator/providers/LifiProvider.js +14 -2
  19. package/dist/packages/core/src/web3/aggregator/providers/OneInchProvider.js +3 -1
  20. package/dist/packages/core/src/web3/aggregator/providers/OpBridgeProvider.js +10 -2
  21. package/dist/packages/core/src/web3/aggregator/providers/OpenOceanProvider.js +63 -7
  22. package/dist/packages/core/src/web3/aggregator/providers/RelayProvider.js +20 -9
  23. package/dist/packages/core/src/web3/aggregator/providers/ZeroXProvider.js +40 -12
  24. package/dist/packages/core/src/web3/aggregator/routeSelector.js +25 -14
  25. package/dist/packages/core/src/web3/skills/bridgeToken.js +47 -3
  26. package/dist/packages/core/src/web3/skills/swapToken.js +47 -4
  27. package/dist/packages/core/src/web3/utils/tokens.js +12 -12
  28. package/dist/packages/core/src/web3/utils/vaultClient.js +8 -1
  29. package/dist/packages/policy/src/server.js +31 -26
  30. package/package.json +1 -1
  31. package/packages/core/package.json +1 -1
  32. package/packages/core/src/agent/llmProvider.ts +17 -4
  33. package/packages/core/src/agent/osAgent.ts +15 -92
  34. package/packages/core/src/agent/reasoning.ts +16 -125
  35. package/packages/core/src/agent/web3Agent.ts +15 -88
  36. package/packages/core/src/config/parser.ts +22 -7
  37. package/packages/core/src/gateway/server.ts +4 -1
  38. package/packages/core/src/gateway/telegram.ts +5 -5
  39. package/packages/core/src/memory/logger.ts +17 -6
  40. package/packages/core/src/memory/reflection.ts +1 -1
  41. package/packages/core/src/system/skills/audioTranscribe.ts +1 -1
  42. package/packages/core/src/system/skills/summarizeText.ts +1 -1
  43. package/packages/core/src/utils/historySanitizer.ts +24 -16
  44. package/packages/core/src/utils/httpClient.ts +22 -5
  45. package/packages/core/src/utils/llmUtils.ts +139 -0
  46. package/packages/core/src/web3/aggregator/defiRouter.ts +6 -2
  47. package/packages/core/src/web3/aggregator/providerRegistry.ts +7 -1
  48. package/packages/core/src/web3/aggregator/providers/KyberSwapProvider.ts +9 -4
  49. package/packages/core/src/web3/aggregator/providers/LifiProvider.ts +16 -2
  50. package/packages/core/src/web3/aggregator/providers/OneInchProvider.ts +3 -2
  51. package/packages/core/src/web3/aggregator/providers/OpBridgeProvider.ts +11 -2
  52. package/packages/core/src/web3/aggregator/providers/OpenOceanProvider.ts +66 -7
  53. package/packages/core/src/web3/aggregator/providers/RelayProvider.ts +25 -10
  54. package/packages/core/src/web3/aggregator/providers/ZeroXProvider.ts +45 -12
  55. package/packages/core/src/web3/aggregator/routeSelector.ts +29 -16
  56. package/packages/core/src/web3/aggregator/types.ts +3 -0
  57. package/packages/core/src/web3/skills/bridgeToken.ts +19 -4
  58. package/packages/core/src/web3/skills/swapToken.ts +17 -5
  59. package/packages/core/src/web3/utils/tokens.ts +12 -12
  60. package/packages/core/src/web3/utils/vaultClient.ts +11 -1
  61. package/packages/dashboard/dist/assets/{index-BW-OzhTX.js → index-DlwR7UtR.js} +3 -3
  62. package/packages/dashboard/dist/index.html +1 -1
  63. package/packages/dashboard/dist/routers/coingecko.png +0 -0
  64. package/packages/dashboard/package.json +3 -2
  65. package/packages/mcp-server/package.json +5 -3
  66. package/packages/policy/package.json +4 -2
  67. package/packages/policy/src/server.ts +27 -29
  68. package/packages/signer/package.json +2 -1
  69. package/packages/signer/src/server.ts +0 -1
@@ -12,7 +12,8 @@ class OpenAIAdapter {
12
12
  message: {
13
13
  content: response.choices[0].message.content,
14
14
  tool_calls: response.choices[0].message.tool_calls
15
- }
15
+ },
16
+ usage: response.usage ? { total_tokens: response.usage.total_tokens } : undefined
16
17
  };
17
18
  }
18
19
  }
@@ -124,7 +125,8 @@ class AnthropicAdapter {
124
125
  message: {
125
126
  content: contentStr,
126
127
  tool_calls: toolCalls.length > 0 ? toolCalls : undefined
127
- }
128
+ },
129
+ usage: response.usage ? { total_tokens: response.usage.input_tokens + response.usage.output_tokens } : undefined
128
130
  };
129
131
  }
130
132
  }
@@ -162,7 +164,9 @@ class GeminiAdapter {
162
164
  catch (e) { }
163
165
  });
164
166
  }
165
- contents.push({ role: 'model', parts: parts });
167
+ if (parts.length > 0) {
168
+ contents.push({ role: 'model', parts: parts });
169
+ }
166
170
  }
167
171
  else if (m.role === 'tool') {
168
172
  contents.push({
@@ -252,11 +256,16 @@ class GeminiAdapter {
252
256
  }
253
257
  }
254
258
  }
259
+ let totalTokens = 0;
260
+ if (data.usageMetadata && data.usageMetadata.totalTokenCount) {
261
+ totalTokens = data.usageMetadata.totalTokenCount;
262
+ }
255
263
  return {
256
264
  message: {
257
265
  content: contentStr,
258
266
  tool_calls: toolCalls.length > 0 ? toolCalls : undefined
259
- }
267
+ },
268
+ usage: totalTokens > 0 ? { total_tokens: totalTokens } : undefined
260
269
  };
261
270
  }
262
271
  }
@@ -4,9 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.logger = void 0;
7
- exports.getOpenAI = getOpenAI;
8
7
  exports.processOsIntent = processOsIntent;
9
- const openai_1 = require("openai");
10
8
  const parser_1 = require("../config/parser");
11
9
  const logger_1 = require("../memory/logger");
12
10
  const tracker_1 = require("../gateway/tracker");
@@ -15,73 +13,7 @@ const skillManager_1 = require("../utils/skillManager");
15
13
  const registry_1 = require("../plugin/registry");
16
14
  const picocolors_1 = __importDefault(require("picocolors"));
17
15
  exports.logger = new logger_1.Logger();
18
- const PROVIDER_CONFIGS = {
19
- ollama: { baseURL: process.env.OLLAMA_BASE_URL ? `${process.env.OLLAMA_BASE_URL}/v1` : 'http://localhost:11434/v1', requiresApiKey: false },
20
- gemini: { baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/', requiresApiKey: true },
21
- openrouter: { baseURL: 'https://openrouter.ai/api/v1', requiresApiKey: true },
22
- groq: { baseURL: 'https://api.groq.com/openai/v1', requiresApiKey: true },
23
- mistral: { baseURL: 'https://api.mistral.ai/v1', requiresApiKey: true },
24
- xai: { baseURL: 'https://api.x.ai/v1', requiresApiKey: true },
25
- deepseek: { baseURL: 'https://api.deepseek.com', requiresApiKey: true },
26
- openai: { requiresApiKey: true }
27
- };
28
- async function getOpenAI() {
29
- const config = (0, parser_1.loadConfig)();
30
- const vaultKeys = await (0, parser_1.loadApiKeys)();
31
- const providerName = config.llm.provider || 'openai';
32
- const providerConf = PROVIDER_CONFIGS[providerName] || PROVIDER_CONFIGS['openai'];
33
- let apiKey = 'local';
34
- if (providerConf.requiresApiKey) {
35
- apiKey = '';
36
- const keyName = `${providerName}_key`;
37
- apiKey = vaultKeys[keyName] || config.credentials?.[keyName] || '';
38
- if (!apiKey) {
39
- throw new Error(`[Security] No API Key found for ${providerName} in OS Keyring. Please run 'nyxora set-key ${providerName} <key>' or 'nyxora setup'.`);
40
- }
41
- console.log(`[LLM] Using API Key securely unlocked from OS Keyring vault.`);
42
- }
43
- return new openai_1.OpenAI({
44
- baseURL: providerConf.baseURL,
45
- apiKey: apiKey,
46
- timeout: 120 * 1000,
47
- maxRetries: 0
48
- });
49
- }
50
- async function executeWithRetry(requestBuilder, maxRetries = 3) {
51
- let retries = 0;
52
- while (retries <= maxRetries) {
53
- try {
54
- const client = await getOpenAI();
55
- return await requestBuilder(client);
56
- }
57
- catch (error) {
58
- const status = error?.status || error?.response?.status;
59
- // 401 Unauthorized or 400 Bad Request - don't retry, it's fatal
60
- if (status === 401 || status === 400) {
61
- console.error(`[LLM] Fatal Error ${status}: ${error.message}. Aborting.`);
62
- throw error;
63
- }
64
- // 429 Rate Limit - rotate provider/key immediately and retry
65
- if (status === 429) {
66
- console.warn(`[LLM] Rate Limit (429) hit. Rotating key...`);
67
- // getOpenAI() automatically rotates to next key if available
68
- retries++;
69
- if (retries > maxRetries)
70
- throw error;
71
- continue; // Try next key immediately
72
- }
73
- // 500, 502, 503, Timeout, Network error - Exponential Backoff
74
- retries++;
75
- if (retries > maxRetries) {
76
- console.error(`[LLM] Max retries reached.`);
77
- throw error;
78
- }
79
- const delayMs = Math.pow(2, retries) * 1000; // 2s, 4s, 8s
80
- console.warn(`[LLM] API Error (${status || error.message}). Retrying in ${delayMs}ms...`);
81
- await new Promise(resolve => setTimeout(resolve, delayMs));
82
- }
83
- }
84
- }
16
+ const llmUtils_1 = require("../utils/llmUtils");
85
17
  function getSystemPrompt(context = 'os') {
86
18
  const config = (0, parser_1.loadConfig)();
87
19
  const currentDateTime = new Date().toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' });
@@ -122,25 +54,21 @@ async function processOsIntent(input, role = 'user', onProgress, sessionId) {
122
54
  let activeTools = [...registry_1.pluginManager.getAllToolDefinitions()];
123
55
  activeTools = activeTools.filter(t => (0, skillManager_1.isSkillActive)(t.function.name));
124
56
  const { sanitizeHistoryForLLM } = require('../utils/historySanitizer');
125
- const sanitizedHistory = sanitizeHistoryForLLM(history, activeTools);
57
+ const sanitizedHistory = sanitizeHistoryForLLM(history, activeTools, config.llm.provider);
126
58
  let messages = [
127
59
  { role: 'system', content: getSystemPrompt('os') },
128
60
  ...sanitizedHistory
129
61
  ];
130
62
  try {
131
- const context = 'os';
132
- let activeTools = [...registry_1.pluginManager.getAllToolDefinitions()];
133
- activeTools = activeTools.filter(t => (0, skillManager_1.isSkillActive)(t.function.name));
134
- const response = await executeWithRetry(async (client) => {
135
- return await client.chat.completions.create({
63
+ const response = await (0, llmUtils_1.executeWithRetry)(async (client) => {
64
+ return await client.chat({
136
65
  model: config.llm.model,
137
66
  temperature: config.llm.temperature,
138
67
  messages: messages,
139
- tools: activeTools,
140
- tool_choice: "auto",
68
+ tools: activeTools
141
69
  });
142
70
  });
143
- const responseMessage = response.choices[0].message;
71
+ const responseMessage = response.message;
144
72
  tracker_1.Tracker.addMessage();
145
73
  if (response.usage?.total_tokens) {
146
74
  tracker_1.Tracker.addTokens(response.usage.total_tokens, config.llm.provider);
@@ -230,13 +158,13 @@ async function processOsIntent(input, role = 'user', onProgress, sessionId) {
230
158
  return finalContent;
231
159
  }
232
160
  // Second call to get the final answer after tool execution
233
- const secondSanitized = sanitizeHistoryForLLM(exports.logger.getHistory(sessionId), activeTools);
161
+ const secondSanitized = sanitizeHistoryForLLM(exports.logger.getHistory(sessionId), activeTools, config.llm.provider);
234
162
  const secondMessages = [
235
163
  { role: 'system', content: getSystemPrompt('os') },
236
164
  ...secondSanitized
237
165
  ];
238
- const secondResponse = await executeWithRetry(async (client) => {
239
- return await client.chat.completions.create({
166
+ const secondResponse = await (0, llmUtils_1.executeWithRetry)(async (client) => {
167
+ return await client.chat({
240
168
  model: config.llm.model,
241
169
  messages: secondMessages,
242
170
  });
@@ -245,7 +173,7 @@ async function processOsIntent(input, role = 'user', onProgress, sessionId) {
245
173
  tracker_1.Tracker.addTokens(secondResponse.usage.total_tokens, config.llm.provider);
246
174
  }
247
175
  tracker_1.Tracker.addEvent('llm.final_response', { provider: config.llm.provider });
248
- let finalContent = secondResponse.choices[0].message.content || "";
176
+ let finalContent = secondResponse.message.content || "";
249
177
  // Clean up orphaned <think> blocks that forgot to output </think>
250
178
  finalContent = finalContent.replace(/<thought>[\s\S]*?<\/thought>\n?/gi, '');
251
179
  finalContent = finalContent.replace(/<think>[\s\S]*?<\/think>\n?/gi, '');
@@ -270,7 +198,11 @@ async function processOsIntent(input, role = 'user', onProgress, sessionId) {
270
198
  }
271
199
  catch (error) {
272
200
  console.error("LLM Error:", error);
273
- const errorMsg = '⚠️ All models are temporarily rate-limited. Please try again in a few minutes.';
201
+ const status = error?.status || error?.response?.status;
202
+ let errorMsg = '⚠️ All models are temporarily rate-limited. Please try again in a few minutes.';
203
+ if (status === 400 || (error.message && error.message.toLowerCase().includes('invalid'))) {
204
+ errorMsg = '⚠️ Terjadi kesalahan pemahaman instruksi. LLM kesulitan menentukan format alat (skill) yang cocok. Silakan coba deskripsikan perintah Anda dengan lebih spesifik.';
205
+ }
274
206
  exports.logger.addEntry({ role: 'assistant', content: errorMsg }, sessionId);
275
207
  return errorMsg;
276
208
  }
@@ -4,121 +4,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.logger = void 0;
7
- exports.getOpenAI = getOpenAI;
8
- exports.getLLMClient = getLLMClient;
9
7
  exports.processUserInput = processUserInput;
10
8
  const fs_1 = __importDefault(require("fs"));
11
- const openai_1 = require("openai");
12
- const llmProvider_1 = require("./llmProvider");
13
- const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
14
9
  const parser_1 = require("../config/parser");
15
10
  const logger_1 = require("../memory/logger");
16
11
  const episodic_1 = require("../memory/episodic");
17
12
  const paths_1 = require("../config/paths");
18
13
  const picocolors_1 = __importDefault(require("picocolors"));
19
14
  exports.logger = new logger_1.Logger();
20
- const PROVIDER_CONFIGS = {
21
- ollama: { baseURL: process.env.OLLAMA_BASE_URL ? `${process.env.OLLAMA_BASE_URL}/v1` : 'http://localhost:11434/v1', requiresApiKey: false },
22
- gemini: { baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/', requiresApiKey: true },
23
- openrouter: { baseURL: 'https://openrouter.ai/api/v1', requiresApiKey: true },
24
- groq: { baseURL: 'https://api.groq.com/openai/v1', requiresApiKey: true },
25
- mistral: { baseURL: 'https://api.mistral.ai/v1', requiresApiKey: true },
26
- xai: { baseURL: 'https://api.x.ai/v1', requiresApiKey: true },
27
- deepseek: { baseURL: 'https://api.deepseek.com', requiresApiKey: true },
28
- openai: { requiresApiKey: true }
29
- };
30
- async function getOpenAI() {
31
- const config = (0, parser_1.loadConfig)();
32
- const vaultKeys = await (0, parser_1.loadApiKeys)();
33
- const providerName = config.llm.provider || 'openai';
34
- // Audio Transcription Fallback: Always try to use OpenAI/Groq if Anthropic/Gemini
35
- let actualProvider = (providerName === 'anthropic' || providerName === 'gemini') ? 'openai' : providerName;
36
- const providerConf = PROVIDER_CONFIGS[actualProvider] || PROVIDER_CONFIGS['openai'];
37
- let apiKey = 'local';
38
- if (providerConf.requiresApiKey) {
39
- const keyName = `${actualProvider}_key`;
40
- apiKey = vaultKeys[keyName] || config.credentials?.[keyName] || '';
41
- if (!apiKey && actualProvider === 'openai') {
42
- // Last resort fallback to groq for audio if openai key is missing
43
- actualProvider = 'groq';
44
- apiKey = vaultKeys['groq_key'] || config.credentials?.['groq_key'] || '';
45
- }
46
- if (!apiKey) {
47
- throw new Error(`[Security] No Audio Transcription API Key found (OpenAI/Groq). Please run 'nyxora set-key openai <key>'.`);
48
- }
49
- }
50
- return new openai_1.OpenAI({
51
- baseURL: (PROVIDER_CONFIGS[actualProvider] || PROVIDER_CONFIGS['openai']).baseURL,
52
- apiKey: apiKey,
53
- timeout: 120 * 1000,
54
- maxRetries: 0
55
- });
56
- }
57
- async function getLLMClient() {
58
- const config = (0, parser_1.loadConfig)();
59
- const vaultKeys = await (0, parser_1.loadApiKeys)();
60
- const providerName = config.llm.provider || 'openai';
61
- let apiKey = '';
62
- const keyName = `${providerName}_key`;
63
- apiKey = vaultKeys[keyName] || config.credentials?.[keyName] || '';
64
- if (!apiKey && providerName !== 'ollama') {
65
- throw new Error(`[Security] No API Key found for ${providerName} in OS Keyring. Please run 'nyxora set-key ${providerName} <key>' or 'nyxora setup'.`);
66
- }
67
- if (providerName !== 'ollama') {
68
- console.log(`[LLM] Using API Key securely unlocked from OS Keyring vault for ${providerName}.`);
69
- }
70
- if (providerName === 'anthropic') {
71
- const client = new sdk_1.default({ apiKey });
72
- return new llmProvider_1.AnthropicAdapter(client);
73
- }
74
- if (providerName === 'gemini') {
75
- return new llmProvider_1.GeminiAdapter(apiKey);
76
- }
77
- // Default fallback (OpenAI, Groq, OpenRouter, xAI, Mistral, DeepSeek)
78
- const providerConf = PROVIDER_CONFIGS[providerName] || PROVIDER_CONFIGS['openai'];
79
- const client = new openai_1.OpenAI({
80
- baseURL: providerConf.baseURL,
81
- apiKey: apiKey || 'local',
82
- timeout: 120 * 1000,
83
- maxRetries: 0
84
- });
85
- return new llmProvider_1.OpenAIAdapter(client);
86
- }
87
- async function executeWithRetry(requestBuilder, maxRetries = 3) {
88
- let retries = 0;
89
- while (retries <= maxRetries) {
90
- try {
91
- const client = await getLLMClient();
92
- return await requestBuilder(client);
93
- }
94
- catch (error) {
95
- const status = error?.status || error?.response?.status;
96
- // 401 Unauthorized or 400 Bad Request - don't retry, it's fatal
97
- if (status === 401 || status === 400) {
98
- console.error(`[LLM] Fatal Error ${status}: ${error.message}. Aborting.`);
99
- throw error;
100
- }
101
- // 429 Rate Limit - rotate provider/key immediately and retry
102
- if (status === 429) {
103
- console.warn(`[LLM] Rate Limit (429) hit. Rotating key...`);
104
- // getOpenAI() automatically rotates to next key if available
105
- retries++;
106
- if (retries > maxRetries)
107
- throw error;
108
- continue; // Try next key immediately
109
- }
110
- // 500, 502, 503, Timeout, Network error - Exponential Backoff
111
- retries++;
112
- if (retries > maxRetries) {
113
- console.error(`[LLM] Max retries reached.`);
114
- throw error;
115
- }
116
- const delayMs = Math.pow(2, retries) * 1000; // 2s, 4s, 8s
117
- console.warn(`[LLM] API Error (${status || error.message}). Retrying in ${delayMs}ms...`);
118
- await new Promise(resolve => setTimeout(resolve, delayMs));
119
- }
120
- }
121
- }
15
+ const llmUtils_1 = require("../utils/llmUtils");
122
16
  function getSystemPrompt(context = 'general') {
123
17
  const config = (0, parser_1.loadConfig)();
124
18
  const currentDateTime = new Date().toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' });
@@ -283,7 +177,7 @@ Reply with EXACTLY ONE WORD.`;
283
177
  ];
284
178
  let context = 'general';
285
179
  try {
286
- const routerResponse = await executeWithRetry(async (client) => {
180
+ const routerResponse = await (0, llmUtils_1.executeWithRetry)(async (client) => {
287
181
  return await client.chat({
288
182
  model: config.llm.model,
289
183
  messages: routerMessages,
@@ -314,21 +208,20 @@ Reply with EXACTLY ONE WORD.`;
314
208
  // General Agent: Use osAgent logic but without execution tools to save tokens.
315
209
  // Wait, osAgent has activeTools. We can just route to osAgent for now, but general agent shouldn't have tools.
316
210
  // Let's create a dummy general intent processing directly here.
317
- const config = (0, parser_1.loadConfig)();
318
211
  exports.logger.addEntry({ role, content: input }, sessionId);
319
- const history = exports.logger.getHistory(sessionId);
320
212
  const messages = [
321
- { role: 'system', content: `You are Nyxora's General Agent. Reason internally. Never reveal private reasoning. Provide only concise conclusions, assumptions, and actionable steps.\n\nCRITICAL RULE: STRICT LANGUAGE MATCHING. Reply in the exact same language as the user's LATEST prompt.` },
322
- ...textOnlyHistory
213
+ { role: 'system', content: getSystemPrompt('general') },
214
+ ...textOnlyHistory,
215
+ { role: 'user', content: input }
323
216
  ];
324
217
  try {
325
- const response = await executeWithRetry(async (client) => {
218
+ const response = await (0, llmUtils_1.executeWithRetry)(async (client) => {
326
219
  return await client.chat({
327
220
  model: config.llm.model,
328
221
  messages: messages
329
222
  });
330
223
  });
331
- let finalContent = response.message?.content || "Maaf, permintaan Anda diblokir oleh Safety Filter AI.";
224
+ let finalContent = response.message?.content || "";
332
225
  finalContent = finalContent.replace(/<thought>[\s\S]*?<\/thought>\n?/gi, '');
333
226
  finalContent = finalContent.replace(/<think>[\s\S]*?<\/think>\n?/gi, '');
334
227
  if (finalContent.includes('<think>')) {
@@ -336,12 +229,19 @@ Reply with EXACTLY ONE WORD.`;
336
229
  finalContent = finalContent.replace(/<think>[\s\S]*$/i, '');
337
230
  }
338
231
  finalContent = finalContent.trim();
232
+ if (!finalContent) {
233
+ finalContent = "⚠️ LLM mengembalikan respons kosong atau terputus. Hal ini biasanya terjadi karena fluktuasi koneksi API atau limitasi sesaat. Silakan coba lagi.";
234
+ }
339
235
  exports.logger.addEntry({ role: 'assistant', content: finalContent }, sessionId);
340
236
  return finalContent;
341
237
  }
342
238
  catch (error) {
343
239
  console.error("General LLM Error:", error);
344
- const errorMsg = '⚠️ Sistem sedang mengalami limitasi koneksi LLM (Rate Limit). Harap tunggu beberapa detik dan coba lagi.';
240
+ const status = error?.status || error?.response?.status;
241
+ let errorMsg = '⚠️ Sistem sedang mengalami limitasi koneksi LLM (Rate Limit). Harap tunggu beberapa detik dan coba lagi.';
242
+ if (status === 400 || (error.message && error.message.toLowerCase().includes('invalid'))) {
243
+ errorMsg = '⚠️ Terjadi kesalahan. Format pesan atau alat (skill) tidak dimengerti oleh LLM.';
244
+ }
345
245
  exports.logger.addEntry({ role: 'assistant', content: errorMsg }, sessionId);
346
246
  return errorMsg;
347
247
  }
@@ -4,9 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.logger = void 0;
7
- exports.getOpenAI = getOpenAI;
8
7
  exports.processWeb3Intent = processWeb3Intent;
9
- const openai_1 = require("openai");
10
8
  const parser_1 = require("../config/parser");
11
9
  const logger_1 = require("../memory/logger");
12
10
  const tracker_1 = require("../gateway/tracker");
@@ -15,73 +13,7 @@ const skillManager_1 = require("../utils/skillManager");
15
13
  const registry_1 = require("../plugin/registry");
16
14
  const picocolors_1 = __importDefault(require("picocolors"));
17
15
  exports.logger = new logger_1.Logger();
18
- const PROVIDER_CONFIGS = {
19
- ollama: { baseURL: process.env.OLLAMA_BASE_URL ? `${process.env.OLLAMA_BASE_URL}/v1` : 'http://localhost:11434/v1', requiresApiKey: false },
20
- gemini: { baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/', requiresApiKey: true },
21
- openrouter: { baseURL: 'https://openrouter.ai/api/v1', requiresApiKey: true },
22
- groq: { baseURL: 'https://api.groq.com/openai/v1', requiresApiKey: true },
23
- mistral: { baseURL: 'https://api.mistral.ai/v1', requiresApiKey: true },
24
- xai: { baseURL: 'https://api.x.ai/v1', requiresApiKey: true },
25
- deepseek: { baseURL: 'https://api.deepseek.com', requiresApiKey: true },
26
- openai: { requiresApiKey: true }
27
- };
28
- async function getOpenAI() {
29
- const config = (0, parser_1.loadConfig)();
30
- const vaultKeys = await (0, parser_1.loadApiKeys)();
31
- const providerName = config.llm.provider || 'openai';
32
- const providerConf = PROVIDER_CONFIGS[providerName] || PROVIDER_CONFIGS['openai'];
33
- let apiKey = 'local';
34
- if (providerConf.requiresApiKey) {
35
- apiKey = '';
36
- const keyName = `${providerName}_key`;
37
- apiKey = vaultKeys[keyName] || config.credentials?.[keyName] || '';
38
- if (!apiKey) {
39
- throw new Error(`[Security] No API Key found for ${providerName} in OS Keyring. Please run 'nyxora set-key ${providerName} <key>' or 'nyxora setup'.`);
40
- }
41
- console.log(`[LLM] Using API Key securely unlocked from OS Keyring vault.`);
42
- }
43
- return new openai_1.OpenAI({
44
- baseURL: providerConf.baseURL,
45
- apiKey: apiKey,
46
- timeout: 120 * 1000,
47
- maxRetries: 0
48
- });
49
- }
50
- async function executeWithRetry(requestBuilder, maxRetries = 3) {
51
- let retries = 0;
52
- while (retries <= maxRetries) {
53
- try {
54
- const client = await getOpenAI();
55
- return await requestBuilder(client);
56
- }
57
- catch (error) {
58
- const status = error?.status || error?.response?.status;
59
- // 401 Unauthorized or 400 Bad Request - don't retry, it's fatal
60
- if (status === 401 || status === 400) {
61
- console.error(`[LLM] Fatal Error ${status}: ${error.message}. Aborting.`);
62
- throw error;
63
- }
64
- // 429 Rate Limit - rotate provider/key immediately and retry
65
- if (status === 429) {
66
- console.warn(`[LLM] Rate Limit (429) hit. Rotating key...`);
67
- // getOpenAI() automatically rotates to next key if available
68
- retries++;
69
- if (retries > maxRetries)
70
- throw error;
71
- continue; // Try next key immediately
72
- }
73
- // 500, 502, 503, Timeout, Network error - Exponential Backoff
74
- retries++;
75
- if (retries > maxRetries) {
76
- console.error(`[LLM] Max retries reached.`);
77
- throw error;
78
- }
79
- const delayMs = Math.pow(2, retries) * 1000; // 2s, 4s, 8s
80
- console.warn(`[LLM] API Error (${status || error.message}). Retrying in ${delayMs}ms...`);
81
- await new Promise(resolve => setTimeout(resolve, delayMs));
82
- }
83
- }
84
- }
16
+ const llmUtils_1 = require("../utils/llmUtils");
85
17
  function getSystemPrompt(context = 'web3') {
86
18
  const config = (0, parser_1.loadConfig)();
87
19
  const currentDateTime = new Date().toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' });
@@ -128,26 +60,24 @@ async function processWeb3Intent(input, role = 'user', onProgress, sessionId) {
128
60
  let activeTools = [...pluginTools];
129
61
  activeTools = activeTools.filter(t => (0, skillManager_1.isSkillActive)(t.function.name));
130
62
  const { sanitizeHistoryForLLM } = require('../utils/historySanitizer');
131
- const sanitizedHistory = sanitizeHistoryForLLM(history, activeTools);
63
+ const sanitizedHistory = sanitizeHistoryForLLM(history, activeTools, config.llm.provider);
132
64
  let messages = [
133
65
  { role: 'system', content: getSystemPrompt('web3') },
134
66
  ...sanitizedHistory
135
67
  ];
136
68
  try {
137
69
  const context = 'web3';
138
- const response = await executeWithRetry(async (client) => {
70
+ const response = await (0, llmUtils_1.executeWithRetry)(async (client) => {
139
71
  // Debug log to find out why Gemini 400 error happens
140
72
  console.log(`[LLM Debug] Sending ${messages.length} messages to LLM.`);
141
- console.log(JSON.stringify(messages, null, 2));
142
- return await client.chat.completions.create({
73
+ return await client.chat({
143
74
  model: config.llm.model,
144
75
  temperature: config.llm.temperature,
145
76
  messages: messages,
146
- tools: activeTools,
147
- tool_choice: "auto",
77
+ tools: activeTools
148
78
  });
149
79
  });
150
- const responseMessage = response.choices[0].message;
80
+ const responseMessage = response.message;
151
81
  tracker_1.Tracker.addMessage();
152
82
  if (response.usage?.total_tokens) {
153
83
  tracker_1.Tracker.addTokens(response.usage.total_tokens, config.llm.provider);
@@ -237,13 +167,13 @@ async function processWeb3Intent(input, role = 'user', onProgress, sessionId) {
237
167
  return finalContent;
238
168
  }
239
169
  // Second call to get the final answer after tool execution
240
- const secondSanitized = sanitizeHistoryForLLM(exports.logger.getHistory(sessionId), activeTools);
170
+ const secondSanitized = sanitizeHistoryForLLM(exports.logger.getHistory(sessionId), activeTools, config.llm.provider);
241
171
  const secondMessages = [
242
172
  { role: 'system', content: getSystemPrompt('web3') },
243
173
  ...secondSanitized
244
174
  ];
245
- const secondResponse = await executeWithRetry(async (client) => {
246
- return await client.chat.completions.create({
175
+ const secondResponse = await (0, llmUtils_1.executeWithRetry)(async (client) => {
176
+ return await client.chat({
247
177
  model: config.llm.model,
248
178
  messages: secondMessages,
249
179
  });
@@ -252,7 +182,7 @@ async function processWeb3Intent(input, role = 'user', onProgress, sessionId) {
252
182
  tracker_1.Tracker.addTokens(secondResponse.usage.total_tokens, config.llm.provider);
253
183
  }
254
184
  tracker_1.Tracker.addEvent('llm.final_response', { provider: config.llm.provider });
255
- let finalContent = secondResponse.choices[0].message.content || "";
185
+ let finalContent = secondResponse.message.content || "";
256
186
  // Clean up orphaned <think> blocks that forgot to output </think>
257
187
  finalContent = finalContent.replace(/<thought>[\s\S]*?<\/thought>\n?/gi, '');
258
188
  finalContent = finalContent.replace(/<think>[\s\S]*?<\/think>\n?/gi, '');
@@ -277,7 +207,11 @@ async function processWeb3Intent(input, role = 'user', onProgress, sessionId) {
277
207
  }
278
208
  catch (error) {
279
209
  console.error("LLM Error:", error);
280
- const errorMsg = '⚠️ All models are temporarily rate-limited. Please try again in a few minutes.';
210
+ const status = error?.status || error?.response?.status;
211
+ let errorMsg = '⚠️ All models are temporarily rate-limited. Please try again in a few minutes.';
212
+ if (status === 400 || (error.message && error.message.toLowerCase().includes('invalid'))) {
213
+ errorMsg = '⚠️ Terjadi kesalahan pemahaman instruksi. LLM kesulitan menentukan format alat (skill) yang cocok. Silakan coba deskripsikan perintah Anda dengan lebih spesifik.';
214
+ }
281
215
  exports.logger.addEntry({ role: 'assistant', content: errorMsg }, sessionId);
282
216
  return errorMsg;
283
217
  }
@@ -121,7 +121,13 @@ async function saveApiKeys(newKeys) {
121
121
  config.credentials = { ...config.credentials, ...newKeys };
122
122
  saveConfig(config);
123
123
  }
124
+ let cachedNyxoraConfig = null;
125
+ let lastConfigLoadTime = 0;
124
126
  function loadConfig() {
127
+ const now = Date.now();
128
+ if (cachedNyxoraConfig && now - lastConfigLoadTime < 5000) {
129
+ return cachedNyxoraConfig;
130
+ }
125
131
  const configPath = (0, paths_1.getPath)('config.yaml');
126
132
  const rpcPath = (0, paths_1.getPath)('rpc_key.yaml');
127
133
  let rpcUrls = loadRpcConfig();
@@ -208,6 +214,8 @@ function loadConfig() {
208
214
  },
209
215
  skills: parsed.skills
210
216
  };
217
+ cachedNyxoraConfig = validatedConfig;
218
+ lastConfigLoadTime = Date.now();
211
219
  return validatedConfig;
212
220
  }
213
221
  catch (error) {
@@ -220,7 +228,7 @@ function loadConfig() {
220
228
  else {
221
229
  console.error('[Config] Failed to load config.yaml. Using default configuration.', error);
222
230
  }
223
- return {
231
+ const defaultConfig = {
224
232
  agent: {
225
233
  name: "Nyxora-Default",
226
234
  description: "Your Personal Web3 Assistant.",
@@ -245,11 +253,16 @@ function loadConfig() {
245
253
  telegram: { enabled: false }
246
254
  }
247
255
  };
256
+ cachedNyxoraConfig = defaultConfig;
257
+ lastConfigLoadTime = Date.now();
258
+ return defaultConfig;
248
259
  }
249
260
  }
250
261
  function saveConfig(newConfig) {
251
262
  const configPath = (0, paths_1.getPath)('config.yaml');
252
263
  try {
264
+ cachedNyxoraConfig = null;
265
+ lastConfigLoadTime = 0;
253
266
  const configToSave = JSON.parse(JSON.stringify(newConfig));
254
267
  if (configToSave.web3 && configToSave.web3.rpc_urls) {
255
268
  delete configToSave.web3.rpc_urls;
@@ -8,6 +8,9 @@ exports.startServer = startServer;
8
8
  const express_1 = __importDefault(require("express"));
9
9
  const cors_1 = __importDefault(require("cors"));
10
10
  const httpClient_1 = require("../utils/httpClient");
11
+ BigInt.prototype.toJSON = function () {
12
+ return this.toString();
13
+ };
11
14
  process.on('unhandledRejection', (reason, promise) => {
12
15
  console.error('[Anti-Crash] Unhandled Rejection at:', promise, 'reason:', reason);
13
16
  });
@@ -180,7 +183,7 @@ app.post('/api/upload-google-credentials', (req, res) => {
180
183
  app.get('/api/history', (req, res) => {
181
184
  try {
182
185
  const sessionId = req.query.session_id;
183
- const history = reasoning_1.logger.getHistory(sessionId);
186
+ const history = reasoning_1.logger.getHistory(sessionId, 1000);
184
187
  // Filter out internal system prompt for the frontend
185
188
  const cleanHistory = history.filter((msg) => msg.role !== 'system');
186
189
  res.json(cleanHistory);
@@ -135,7 +135,7 @@ function startTelegramBot() {
135
135
  }
136
136
  catch (e) { }
137
137
  };
138
- const response = await (0, reasoning_1.processUserInput)(text, 'user', onProgress, ctx.chat?.id.toString());
138
+ const response = await (0, reasoning_1.processUserInput)(text, 'user', onProgress, `telegram_${ctx.chat?.id}`);
139
139
  if (progressMsgId) {
140
140
  await ctx.api.deleteMessage(ctx.chat.id, progressMsgId).catch(() => { });
141
141
  }
@@ -189,7 +189,7 @@ function startTelegramBot() {
189
189
  }
190
190
  catch (e) { }
191
191
  };
192
- const response = await (0, reasoning_1.processUserInput)(prompt, 'user', onProgress, ctx.chat?.id.toString());
192
+ const response = await (0, reasoning_1.processUserInput)(prompt, 'user', onProgress, `telegram_${ctx.chat?.id}`);
193
193
  if (progressMsgId) {
194
194
  await ctx.api.deleteMessage(ctx.chat.id, progressMsgId).catch(() => { });
195
195
  }
@@ -254,19 +254,19 @@ function startTelegramBot() {
254
254
  transactionManager_1.txManager.updateStatus(txId, 'executed', result);
255
255
  const prettyMsg = (0, formatter_1.formatTransactionSuccess)(tx, result);
256
256
  await ctx.reply(formatToTelegramHTML(`✅ **Transaction processed: Success**\n\n${prettyMsg}`), { parse_mode: 'HTML' });
257
- (0, reasoning_1.processUserInput)(`Transaction ${txId} was APPROVED via Telegram. Result: ${result}`, 'system', undefined, ctx.chat?.id.toString()).catch(() => { });
257
+ (0, reasoning_1.processUserInput)(`Transaction ${txId} was APPROVED via Telegram. Result: ${result}`, 'system', undefined, `telegram_${ctx.chat?.id}`).catch(() => { });
258
258
  }
259
259
  catch (err) {
260
260
  transactionManager_1.txManager.updateStatus(txId, 'failed', err.message);
261
261
  const prettyError = (0, formatter_1.formatTransactionError)(tx, err.message);
262
262
  await ctx.reply(prettyError);
263
- (0, reasoning_1.processUserInput)(`Transaction ${txId} FAILED via Telegram. Error: ${err.message}`, 'system', undefined, ctx.chat?.id.toString()).catch(() => { });
263
+ (0, reasoning_1.processUserInput)(`Transaction ${txId} FAILED via Telegram. Error: ${err.message}`, 'system', undefined, `telegram_${ctx.chat?.id}`).catch(() => { });
264
264
  }
265
265
  });
266
266
  bot.callbackQuery(/^reject_(.+)$/, async (ctx) => {
267
267
  const txId = ctx.match[1];
268
268
  transactionManager_1.txManager.updateStatus(txId, 'rejected');
269
- (0, reasoning_1.processUserInput)(`Transaction ${txId} was REJECTED via Telegram. CRITICAL: DO NOT retry or recreate this transaction. Acknowledge this cancellation to the user and stop.`, 'system', undefined, ctx.chat?.id.toString()).catch(() => { });
269
+ (0, reasoning_1.processUserInput)(`Transaction ${txId} was REJECTED via Telegram. CRITICAL: DO NOT retry or recreate this transaction. Acknowledge this cancellation to the user and stop.`, 'system', undefined, `telegram_${ctx.chat?.id}`).catch(() => { });
270
270
  await ctx.answerCallbackQuery('Transaction cancelled.');
271
271
  await ctx.reply(`❌ Transaction cancelled.`);
272
272
  await ctx.api.editMessageReplyMarkup(ctx.chat.id, ctx.msg.message_id, { reply_markup: { inline_keyboard: [] } }).catch(() => { });