neoagent 2.4.1-beta.41 → 2.4.1-beta.43

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.
@@ -231,7 +231,7 @@ async function getProviderForUser(userId, task = '', isSubagent = false, modelOv
231
231
  };
232
232
  }
233
233
 
234
- async function getFailureFallbackModelId(userId, agentId, currentModelId, preferredFallbackId = null) {
234
+ async function getFailureFallbackModelId(userId, agentId, currentModelId, preferredFallbackId = null, failureError = null) {
235
235
  const { getSupportedModels } = require('./models');
236
236
  const aiSettings = getAiSettings(userId, agentId);
237
237
  const models = await getSupportedModels(userId, agentId);
@@ -247,7 +247,12 @@ async function getFailureFallbackModelId(userId, agentId, currentModelId, prefer
247
247
  || availableModels.find((model) => model.id === currentModelId)
248
248
  || null;
249
249
 
250
- if (preferredFallbackId && preferredFallbackId !== currentModelId) {
250
+ // When the failure is a provider-level rate limit, the preferred fallback is
251
+ // likely on the same provider and will hit the same limit. Skip it and prefer
252
+ // a fallback from a different provider instead.
253
+ const isProviderRateLimit = /429|rate.?limit|free-models-per/i.test(String(failureError?.message || ''));
254
+
255
+ if (preferredFallbackId && preferredFallbackId !== currentModelId && !isProviderRateLimit) {
251
256
  const preferred = pool.find((model) => model.id === preferredFallbackId)
252
257
  || availableModels.find((model) => model.id === preferredFallbackId);
253
258
  if (preferred) return preferred.id;
@@ -259,6 +264,14 @@ async function getFailureFallbackModelId(userId, agentId, currentModelId, prefer
259
264
  if (differentProvider) return differentProvider.id;
260
265
  }
261
266
 
267
+ // If no different-provider model exists, still try the preferred fallback
268
+ // even on rate limits (it's better than nothing).
269
+ if (preferredFallbackId && preferredFallbackId !== currentModelId) {
270
+ const preferred = pool.find((model) => model.id === preferredFallbackId)
271
+ || availableModels.find((model) => model.id === preferredFallbackId);
272
+ if (preferred) return preferred.id;
273
+ }
274
+
262
275
  const differentModel = pool.find((model) => model.id !== currentModelId)
263
276
  || availableModels.find((model) => model.id !== currentModelId);
264
277
  return differentModel?.id || null;
@@ -1093,7 +1106,9 @@ class AgentEngine {
1093
1106
  }
1094
1107
 
1095
1108
  getMessagingRetryLimit(maxIterations) {
1096
- return Math.max(1, maxIterations);
1109
+ // Cap at 3: more than 3 autonomous messaging retries indicates a structural
1110
+ // problem (model unavailable, bad config) that more retries won't solve.
1111
+ return Math.min(3, Math.max(1, maxIterations));
1097
1112
  }
1098
1113
 
1099
1114
  buildContextMessages(systemPrompt, summaryMessage, historyMessages, recallMsg) {
@@ -1234,7 +1249,7 @@ class AgentEngine {
1234
1249
  let model = selectedProvider.model;
1235
1250
  let providerName = selectedProvider.providerName;
1236
1251
  const switchToFallbackModel = async (failedModel, error, phase) => {
1237
- const fallbackModelId = await getFailureFallbackModelId(userId, agentId, failedModel, aiSettings.fallback_model_id);
1252
+ const fallbackModelId = await getFailureFallbackModelId(userId, agentId, failedModel, aiSettings.fallback_model_id, error);
1238
1253
  if (!fallbackModelId || fallbackModelId === failedModel) return false;
1239
1254
  console.log(`[Engine] ${phase} failed on ${failedModel}; attempting fallback to: ${fallbackModelId}`);
1240
1255
  this.emit(userId, 'run:interim', {
@@ -1700,7 +1715,7 @@ class AgentEngine {
1700
1715
  } catch (err) {
1701
1716
  console.error(`[Engine] Model call failed (${model}):`, err.message);
1702
1717
  const fallbackModelId = retryForFallback
1703
- ? await getFailureFallbackModelId(userId, agentId, model, aiSettings.fallback_model_id)
1718
+ ? await getFailureFallbackModelId(userId, agentId, model, aiSettings.fallback_model_id, err)
1704
1719
  : null;
1705
1720
  if (fallbackModelId) {
1706
1721
  const failedModel = model;
@@ -2503,11 +2518,16 @@ class AgentEngine {
2503
2518
 
2504
2519
  const runMeta = this.activeRuns.get(runId);
2505
2520
  const retryCount = Number(options.messagingAutonomousRetryCount || 0);
2521
+ // Rate-limit errors (429) must not trigger messaging retries: the model
2522
+ // won't be available in the milliseconds between retries, so spawning new
2523
+ // runs just compounds the rate-limit pressure with no benefit.
2524
+ const isRateLimitError = /429|rate.?limit|free-models-per/i.test(String(err?.message || ''));
2506
2525
  const canRetryMessagingRun = (
2507
2526
  triggerSource === 'messaging'
2508
2527
  && options.source
2509
2528
  && options.chatId
2510
2529
  && err?.disableAutonomousRetry !== true
2530
+ && !isRateLimitError
2511
2531
  && retryCount < this.getMessagingRetryLimit(maxIterations)
2512
2532
  );
2513
2533
 
@@ -224,6 +224,14 @@ async function refreshProviderModelList(providerId, apiKey, baseUrl) {
224
224
  return models;
225
225
  } catch (err) {
226
226
  console.warn(`[Models] Failed to refresh ${providerId} models:`, err.message);
227
+ // Always record a lastRefresh so we don't hammer the API on every request.
228
+ // Permanent errors (auth/billing/credits) get a longer backoff.
229
+ const isPermanent = /401|403|unauthorized|forbidden|credits|spending/i.test(err.message);
230
+ const backoff = isPermanent ? 30 * 60 * 1000 : DYNAMIC_REFRESH_INTERVAL;
231
+ providerModelCache.set(cacheKey, {
232
+ models: existing?.models || [],
233
+ lastRefresh: now - DYNAMIC_REFRESH_INTERVAL + backoff,
234
+ });
227
235
  return existing?.models || [];
228
236
  }
229
237
  }
@@ -16,6 +16,8 @@ class OpenRouterProvider extends OpenAICompatibleProvider {
16
16
  this.client = new OpenAI({
17
17
  apiKey: config.apiKey || process.env.OPENROUTER_API_KEY,
18
18
  baseURL: this.baseURL,
19
+ timeout: 90_000, // 90 s — free models can queue; avoids hanging forever
20
+ maxRetries: 0, // engine handles retries; avoid SDK silently re-queuing slow models
19
21
  defaultHeaders: {
20
22
  'HTTP-Referer': 'https://github.com/NeoLabs-Systems/NeoAgent',
21
23
  'X-Title': 'NeoAgent',
@@ -57,6 +59,14 @@ class OpenRouterProvider extends OpenAICompatibleProvider {
57
59
  return params;
58
60
  }
59
61
 
62
+ _extractOpenRouterError(obj) {
63
+ if (!obj) return null;
64
+ const e = obj.error;
65
+ if (!e) return null;
66
+ const msg = e.message || (typeof e === 'string' ? e : JSON.stringify(e));
67
+ return `OpenRouter: ${msg}${e.code ? ` (code ${e.code})` : ''}`;
68
+ }
69
+
60
70
  async chat(messages, tools = [], options = {}) {
61
71
  const model = options.model || this.getDefaultModel();
62
72
  const params = this._buildParams(model, messages, tools, options);
@@ -66,6 +76,12 @@ class OpenRouterProvider extends OpenAICompatibleProvider {
66
76
  } catch (err) {
67
77
  throw new Error(`OpenRouter request failed: ${err?.message || String(err)}`);
68
78
  }
79
+ // OpenRouter returns HTTP 200 even for errors (rate limits, model unavailable, etc.)
80
+ const orErr = this._extractOpenRouterError(response);
81
+ if (orErr) throw new Error(orErr);
82
+ if (!response?.choices?.length) {
83
+ throw new Error(`OpenRouter: model returned no choices (may be rate-limited or unavailable)`);
84
+ }
69
85
  return this.normalizeResponse(response);
70
86
  }
71
87
 
@@ -88,6 +104,9 @@ class OpenRouterProvider extends OpenAICompatibleProvider {
88
104
  let content = '';
89
105
  let finalUsage = null;
90
106
 
107
+ // The OpenAI SDK converts OpenRouter's SSE error events (data.error) into
108
+ // APIErrors before yielding — so we wrap the loop to add context.
109
+ try {
91
110
  for await (const chunk of stream) {
92
111
  if (chunk.usage && (!chunk.choices || chunk.choices.length === 0)) {
93
112
  finalUsage = this.normalizeUsage(chunk.usage);
@@ -127,6 +146,10 @@ class OpenRouterProvider extends OpenAICompatibleProvider {
127
146
  return;
128
147
  }
129
148
  }
149
+ } catch (err) {
150
+ // Re-throw SDK APIErrors (from OpenRouter SSE error events) with OpenRouter context.
151
+ throw new Error(`OpenRouter: ${err?.message || String(err)}`);
152
+ }
130
153
 
131
154
  if (toolCalls.length > 0) {
132
155
  yield { type: 'tool_calls', toolCalls, content, usage: finalUsage };