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.
- package/flutter_app/lib/main_chat.dart +112 -16
- package/package.json +1 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +38423 -38350
- package/server/services/ai/engine.js +25 -5
- package/server/services/ai/models.js +8 -0
- package/server/services/ai/providers/openrouter.js +23 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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 };
|