neoagent 2.4.1-beta.40 → 2.4.1-beta.42

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.
@@ -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 };