neuro-cli 4.3.0 → 5.0.1

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.
@@ -1,6 +1,7 @@
1
1
  // ============================================================
2
2
  // NeuroCLI - OpenRouter API Client
3
3
  // Streaming + Tool Use + Multi-model support
4
+ // Now with RateLimitManager integration
4
5
  // ============================================================
5
6
  // @ts-ignore
6
7
  import { createParser } from 'eventsource-parser';
@@ -9,6 +10,7 @@ export class OpenRouterClient {
9
10
  apiKey;
10
11
  baseUrl;
11
12
  totalUsage = { inputTokens: 0, outputTokens: 0, cost: 0 };
13
+ rateLimiter = null;
12
14
  constructor(apiKey, baseUrl = 'https://openrouter.ai/api/v1') {
13
15
  this.apiKey = apiKey;
14
16
  this.baseUrl = baseUrl;
@@ -20,15 +22,42 @@ export class OpenRouterClient {
20
22
  this.totalUsage = { inputTokens: 0, outputTokens: 0, cost: 0 };
21
23
  }
22
24
  /**
23
- * Main chat completion with streaming
25
+ * Set the rate limiter instance for this client
26
+ */
27
+ setRateLimiter(rateLimiter) {
28
+ this.rateLimiter = rateLimiter;
29
+ }
30
+ /**
31
+ * Main chat completion with streaming + rate limit awareness
24
32
  */
25
33
  async chat(request, callbacks) {
26
34
  const model = MODELS[request.model];
27
35
  if (!model) {
28
36
  throw new Error(`Unknown model: ${request.model}`);
29
37
  }
38
+ // --- Rate limit pre-check ---
39
+ let activeModel = request.model;
40
+ if (this.rateLimiter) {
41
+ const check = await this.rateLimiter.acquire(request.model);
42
+ if (check.suggestedModel) {
43
+ // Switch to the suggested alternative model
44
+ activeModel = check.suggestedModel;
45
+ callbacks?.onThinking?.(`🔄 Rate limiter rotated to ${MODELS[activeModel]?.name || activeModel}`);
46
+ }
47
+ else if (!check.allowed) {
48
+ // Not allowed and no alternative - wait
49
+ if (check.waitMs > 0) {
50
+ callbacks?.onThinking?.(`⏳ Rate limited, waiting ${Math.ceil(check.waitMs / 1000)}s...`);
51
+ await new Promise(resolve => setTimeout(resolve, check.waitMs));
52
+ }
53
+ }
54
+ }
55
+ const activeModelConfig = MODELS[activeModel];
56
+ if (!activeModelConfig) {
57
+ throw new Error(`Unknown model after rate limit rotation: ${activeModel}`);
58
+ }
30
59
  const body = {
31
- model: request.model,
60
+ model: activeModel,
32
61
  messages: request.messages.map(m => ({
33
62
  role: m.role,
34
63
  content: m.content,
@@ -38,9 +67,9 @@ export class OpenRouterClient {
38
67
  })),
39
68
  stream: request.stream ?? true,
40
69
  temperature: request.temperature ?? 0.7,
41
- max_tokens: request.maxTokens ?? model.maxOutput,
70
+ max_tokens: request.maxTokens ?? activeModelConfig.maxOutput,
42
71
  };
43
- if (request.tools && request.tools.length > 0 && model.supportsTools) {
72
+ if (request.tools && request.tools.length > 0 && activeModelConfig.supportsTools) {
44
73
  body.tools = request.tools.map(t => ({
45
74
  type: 'function',
46
75
  function: {
@@ -54,47 +83,133 @@ export class OpenRouterClient {
54
83
  let lastError = null;
55
84
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
56
85
  if (attempt > 0) {
57
- // Exponential backoff: 5s, 15s, 30s
58
- const waitTime = Math.min(5000 * Math.pow(3, attempt - 1), 30000);
59
- callbacks?.onThinking?.(`⏳ Rate limited, retrying in ${waitTime / 1000}s (attempt ${attempt + 1}/${maxRetries + 1})...`);
86
+ // Smart backoff: use rate limiter cooldown if available, otherwise exponential
87
+ let waitTime;
88
+ if (this.rateLimiter) {
89
+ const state = this.rateLimiter.getStatus().find(s => s.model === activeModel);
90
+ waitTime = state && state.cooldown > 0 ? state.cooldown * 1000 : Math.min(5000 * Math.pow(2, attempt - 1), 30000);
91
+ }
92
+ else {
93
+ waitTime = Math.min(5000 * Math.pow(3, attempt - 1), 30000);
94
+ }
95
+ callbacks?.onThinking?.(`⏳ Retrying in ${Math.ceil(waitTime / 1000)}s (attempt ${attempt + 1}/${maxRetries + 1})...`);
60
96
  await new Promise(resolve => setTimeout(resolve, waitTime));
61
97
  }
62
- const response = await fetch(`${this.baseUrl}/chat/completions`, {
63
- method: 'POST',
64
- headers: {
65
- 'Authorization': `Bearer ${this.apiKey}`,
66
- 'Content-Type': 'application/json',
67
- 'HTTP-Referer': 'https://neurocli.dev',
68
- 'X-Title': 'NeuroCLI',
69
- },
70
- body: JSON.stringify(body),
71
- });
72
- if (!response.ok) {
73
- const errorBody = await response.text();
74
- // Parse retry_after from 429 errors
75
- if (response.status === 429) {
76
- try {
77
- const errorData = JSON.parse(errorBody);
78
- const retryAfter = errorData?.error?.metadata?.retry_after_seconds;
79
- if (retryAfter && attempt < maxRetries) {
80
- lastError = new Error(`Rate limited, retry after ${retryAfter}s`);
98
+ const startTime = Date.now();
99
+ try {
100
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
101
+ method: 'POST',
102
+ headers: {
103
+ 'Authorization': `Bearer ${this.apiKey}`,
104
+ 'Content-Type': 'application/json',
105
+ 'HTTP-Referer': 'https://neurocli.dev',
106
+ 'X-Title': 'NeuroCLI',
107
+ },
108
+ body: JSON.stringify(body),
109
+ });
110
+ const responseTimeMs = Date.now() - startTime;
111
+ if (!response.ok) {
112
+ const errorBody = await response.text();
113
+ // Parse retry_after from 429 errors
114
+ if (response.status === 429) {
115
+ let retryAfterSeconds;
116
+ try {
117
+ const errorData = JSON.parse(errorBody);
118
+ retryAfterSeconds = errorData?.error?.metadata?.retry_after_seconds;
119
+ }
120
+ catch { }
121
+ // Record rate limit in manager
122
+ if (this.rateLimiter) {
123
+ this.rateLimiter.recordRateLimit(activeModel, retryAfterSeconds);
124
+ }
125
+ // Try model rotation before retrying
126
+ if (this.rateLimiter && attempt < maxRetries) {
127
+ const nextModel = this.rateLimiter.getNextRotationModel(activeModel);
128
+ if (nextModel && nextModel !== activeModel) {
129
+ const nextModelConfig = MODELS[nextModel];
130
+ if (nextModelConfig) {
131
+ callbacks?.onThinking?.(`🔄 Rotating ${MODELS[activeModel]?.name || activeModel} → ${nextModelConfig.name} due to rate limit`);
132
+ activeModel = nextModel;
133
+ body.model = nextModel;
134
+ body.max_tokens = request.maxTokens ?? nextModelConfig.maxOutput;
135
+ if (request.tools && request.tools.length > 0 && nextModelConfig.supportsTools) {
136
+ // tools already set
137
+ }
138
+ else if (request.tools && !nextModelConfig.supportsTools) {
139
+ delete body.tools;
140
+ }
141
+ // Don't count this as a retry attempt for the new model
142
+ attempt--;
143
+ continue;
144
+ }
145
+ }
146
+ }
147
+ if (retryAfterSeconds && attempt < maxRetries) {
148
+ lastError = new Error(`Rate limited, retry after ${retryAfterSeconds}s`);
81
149
  continue; // Retry
82
150
  }
151
+ if (attempt < maxRetries) {
152
+ lastError = new Error(`Rate limited (429): ${errorBody}`);
153
+ continue;
154
+ }
83
155
  }
84
- catch { }
85
- // If we can't parse retry_after or max retries reached, throw
86
- if (attempt < maxRetries) {
87
- lastError = new Error(`Rate limited (429): ${errorBody}`);
88
- continue;
156
+ // Server errors (5xx) - record and potentially rotate
157
+ if (response.status >= 500) {
158
+ if (this.rateLimiter) {
159
+ this.rateLimiter.recordServerError(activeModel);
160
+ }
161
+ if (attempt < maxRetries) {
162
+ lastError = new Error(`Server error (${response.status}): ${errorBody}`);
163
+ // Try model rotation for server errors too
164
+ if (this.rateLimiter) {
165
+ const nextModel = this.rateLimiter.getNextRotationModel(activeModel);
166
+ if (nextModel && nextModel !== activeModel) {
167
+ const nextModelConfig = MODELS[nextModel];
168
+ if (nextModelConfig) {
169
+ callbacks?.onThinking?.(`🔄 Rotating away from ${MODELS[activeModel]?.name || activeModel} due to server error`);
170
+ activeModel = nextModel;
171
+ body.model = nextModel;
172
+ body.max_tokens = request.maxTokens ?? nextModelConfig.maxOutput;
173
+ if (request.tools && request.tools.length > 0 && nextModelConfig.supportsTools) {
174
+ // tools already set
175
+ }
176
+ else if (request.tools && !nextModelConfig.supportsTools) {
177
+ delete body.tools;
178
+ }
179
+ attempt--;
180
+ continue;
181
+ }
182
+ }
183
+ }
184
+ continue;
185
+ }
89
186
  }
187
+ throw new Error(`OpenRouter API error (${response.status}): ${errorBody}`);
188
+ }
189
+ // Success!
190
+ if (this.rateLimiter) {
191
+ this.rateLimiter.recordSuccess(activeModel, responseTimeMs);
192
+ }
193
+ if (request.stream && callbacks) {
194
+ const result = await this.handleStreamingResponse(response, activeModel, callbacks);
195
+ return result;
196
+ }
197
+ else {
198
+ const result = await this.handleNonStreamingResponse(response, activeModel);
199
+ return result;
90
200
  }
91
- throw new Error(`OpenRouter API error (${response.status}): ${errorBody}`);
92
- }
93
- if (request.stream && callbacks) {
94
- return this.handleStreamingResponse(response, request.model, callbacks);
95
201
  }
96
- else {
97
- return this.handleNonStreamingResponse(response, request.model);
202
+ catch (error) {
203
+ // Network-level errors (fetch failed)
204
+ if (error instanceof TypeError && error.message.includes('fetch')) {
205
+ if (this.rateLimiter) {
206
+ this.rateLimiter.recordServerError(activeModel);
207
+ }
208
+ lastError = error;
209
+ if (attempt < maxRetries)
210
+ continue;
211
+ }
212
+ throw error;
98
213
  }
99
214
  }
100
215
  throw lastError || new Error('Max retries exceeded');
@@ -297,6 +297,17 @@ export class CommandSystem {
297
297
  description: 'Browser automation (navigate/screenshot/click)',
298
298
  prompt: 'Use the browser to navigate, take screenshots, click elements, or extract content from web pages. {args}',
299
299
  },
300
+ // --- Rate Limit Commands ---
301
+ {
302
+ name: 'ratelimit',
303
+ description: 'Show rate limiter status for all models',
304
+ prompt: 'Show the current rate limiter status. Display health scores, cooldowns, 429 counts, and queue status for all tracked models. {args}',
305
+ },
306
+ {
307
+ name: 'ratelimit-reset',
308
+ description: 'Reset rate limiter state',
309
+ prompt: 'Reset all rate limiter cooldowns and health scores. Use this if models are stuck in cooldown. {args}',
310
+ },
300
311
  ];
301
312
  for (const cmd of bundled) {
302
313
  if (!this.commands.has(cmd.name)) {
@@ -14,82 +14,82 @@ export const DEFAULT_CONFIG = {
14
14
  planner: {
15
15
  name: 'Planner',
16
16
  description: 'Task decomposition and planning specialist',
17
- systemPrompt: `You are an expert software architect and planner. Your job is to break down complex tasks into clear, actionable steps. For each step, specify which agent should handle it and what tools they should use. Be thorough but concise.`,
17
+ systemPrompt: `You are an expert software architect and planner. Your job is to break down complex tasks into clear, actionable steps. For each step, specify which agent should handle it and what tools they should use. Be thorough but concise. Keep working until you have a COMPLETE plan covering every aspect of the task.`,
18
18
  model: 'qwen/qwen3-coder:free',
19
19
  temperature: 0.7,
20
20
  maxTokens: 4096,
21
21
  tools: ['read_file', 'search_files', 'list_directory'],
22
- maxIterations: 10,
22
+ maxIterations: 0, // 0 = no limit, run until done
23
23
  },
24
24
  coder: {
25
25
  name: 'Coder',
26
26
  description: 'Code generation and modification specialist',
27
- systemPrompt: `You are an expert software developer. You write clean, efficient, well-documented code. You understand design patterns, best practices, and can work with any programming language or framework. When modifying existing code, you make minimal, targeted changes. Always consider edge cases and error handling.`,
27
+ systemPrompt: `You are an expert software developer. You write clean, efficient, well-documented code. You understand design patterns, best practices, and can work with any programming language or framework. When modifying existing code, you make minimal, targeted changes. Always consider edge cases and error handling. IMPORTANT: You MUST keep working until the entire task is COMPLETE — do not stop after creating only some files. Create ALL necessary files, update ALL imports, and verify your work.`,
28
28
  model: 'qwen/qwen3-coder:free',
29
29
  temperature: 0.4,
30
30
  maxTokens: 16384,
31
31
  tools: ['read_file', 'write_file', 'edit_file', 'search_files', 'list_directory', 'run_command', 'apply_diff'],
32
- maxIterations: 50,
32
+ maxIterations: 0, // 0 = no limit, run until done
33
33
  },
34
34
  reviewer: {
35
35
  name: 'Reviewer',
36
36
  description: 'Code review and quality assurance specialist',
37
- systemPrompt: `You are an expert code reviewer. You analyze code for bugs, security vulnerabilities, performance issues, and adherence to best practices. You provide specific, actionable feedback with line references. You categorize issues by severity (critical, warning, suggestion). You also check for test coverage and documentation.`,
37
+ systemPrompt: `You are an expert code reviewer. You analyze code for bugs, security vulnerabilities, performance issues, and adherence to best practices. You provide specific, actionable feedback with line references. You categorize issues by severity (critical, warning, suggestion). You also check for test coverage and documentation. Keep reviewing until you have checked ALL relevant files.`,
38
38
  model: 'nvidia/nemotron-3-super-120b-a12b:free',
39
39
  temperature: 0.3,
40
40
  maxTokens: 8192,
41
41
  tools: ['read_file', 'search_files', 'list_directory'],
42
- maxIterations: 15,
42
+ maxIterations: 0,
43
43
  },
44
44
  researcher: {
45
45
  name: 'Researcher',
46
46
  description: 'Information gathering and analysis specialist',
47
- systemPrompt: `You are an expert researcher and analyst. You gather information from files, web resources, and codebases. You synthesize findings into clear, structured reports. You can explain complex technical concepts and find relevant examples or documentation.`,
47
+ systemPrompt: `You are an expert researcher and analyst. You gather information from files, web resources, and codebases. You synthesize findings into clear, structured reports. You can explain complex technical concepts and find relevant examples or documentation. Keep researching until you have a COMPLETE picture.`,
48
48
  model: 'google/gemma-4-31b-it:free',
49
49
  temperature: 0.5,
50
50
  maxTokens: 8192,
51
51
  tools: ['read_file', 'search_files', 'list_directory', 'web_search', 'web_fetch'],
52
- maxIterations: 20,
52
+ maxIterations: 0,
53
53
  },
54
54
  tester: {
55
55
  name: 'Tester',
56
56
  description: 'Test generation and execution specialist',
57
- systemPrompt: `You are an expert QA engineer. You write comprehensive test suites covering unit tests, integration tests, and edge cases. You run tests and analyze failures to identify root causes. You ensure code meets quality standards before it's considered complete.`,
57
+ systemPrompt: `You are an expert QA engineer. You write comprehensive test suites covering unit tests, integration tests, and edge cases. You run tests and analyze failures to identify root causes. You ensure code meets quality standards before it's considered complete. Keep testing until ALL tests pass.`,
58
58
  model: 'qwen/qwen3-coder:free',
59
59
  temperature: 0.3,
60
60
  maxTokens: 8192,
61
61
  tools: ['read_file', 'write_file', 'search_files', 'run_command', 'list_directory'],
62
- maxIterations: 25,
62
+ maxIterations: 0,
63
63
  },
64
64
  debugger: {
65
65
  name: 'Debugger',
66
66
  description: 'Bug investigation and fixing specialist',
67
- systemPrompt: `You are an expert debugger. You systematically investigate bugs by reading code, running diagnostic commands, analyzing error messages and stack traces. You identify root causes and implement targeted fixes. You verify fixes work before reporting completion.`,
67
+ systemPrompt: `You are an expert debugger. You systematically investigate bugs by reading code, running diagnostic commands, analyzing error messages and stack traces. You identify root causes and implement targeted fixes. You verify fixes work before reporting completion. Keep debugging until the bug is COMPLETELY fixed and verified.`,
68
68
  model: 'qwen/qwen3-coder:free',
69
69
  temperature: 0.2,
70
70
  maxTokens: 8192,
71
71
  tools: ['read_file', 'edit_file', 'search_files', 'run_command', 'list_directory', 'apply_diff'],
72
- maxIterations: 20,
72
+ maxIterations: 0,
73
73
  },
74
74
  architect: {
75
75
  name: 'Architect',
76
76
  description: 'System design and architecture specialist',
77
- systemPrompt: `You are an expert software architect. You design scalable, maintainable system architectures. You make technology stack decisions, define component boundaries, and establish coding standards. You create detailed technical specifications that other agents can implement.`,
77
+ systemPrompt: `You are an expert software architect. You design scalable, maintainable system architectures. You make technology stack decisions, define component boundaries, and establish coding standards. You create detailed technical specifications that other agents can implement. Keep designing until the architecture is COMPLETE and all components are specified.`,
78
78
  model: 'nvidia/nemotron-3-ultra-550b-a55b:free',
79
79
  temperature: 0.7,
80
80
  maxTokens: 8192,
81
81
  tools: ['read_file', 'search_files', 'list_directory'],
82
- maxIterations: 15,
82
+ maxIterations: 0,
83
83
  },
84
84
  devops: {
85
85
  name: 'DevOps',
86
86
  description: 'Deployment and infrastructure specialist',
87
- systemPrompt: `You are an expert DevOps engineer. You handle CI/CD pipelines, Docker configurations, cloud deployments, and infrastructure as code. You optimize build processes and ensure reliable deployments.`,
87
+ systemPrompt: `You are an expert DevOps engineer. You handle CI/CD pipelines, Docker configurations, cloud deployments, and infrastructure as code. You optimize build processes and ensure reliable deployments. Keep working until the deployment is FULLY configured and verified.`,
88
88
  model: 'cohere/north-mini-code:free',
89
89
  temperature: 0.3,
90
90
  maxTokens: 8192,
91
91
  tools: ['read_file', 'write_file', 'edit_file', 'run_command', 'search_files', 'list_directory'],
92
- maxIterations: 25,
92
+ maxIterations: 0,
93
93
  },
94
94
  },
95
95
  tools: {
@@ -208,7 +208,7 @@ export const DEFAULT_CONFIG = {
208
208
  autoMode: {
209
209
  enabled: false,
210
210
  safetyLevel: 'conservative',
211
- maxIterations: 50,
211
+ maxIterations: 0, // 0 = no limit, run until done (Claude Code style)
212
212
  maxCost: 0,
213
213
  maxTimeMs: 0,
214
214
  blockedCommands: ['rm -rf /', 'mkfs', 'dd if=/dev/zero'],
@@ -57,6 +57,7 @@ import { TerminalUX } from './terminal-ux.js';
57
57
  import { MultiSessionManager } from './multi-session.js';
58
58
  import { GitWorktreeManager } from './git-worktree.js';
59
59
  import { AutoUpdater } from './updater.js';
60
+ import { RateLimitManager } from './rate-limiter.js';
60
61
  export declare class NeuroEngine {
61
62
  config: NeuroConfig;
62
63
  client: OpenRouterClient;
@@ -117,6 +118,7 @@ export declare class NeuroEngine {
117
118
  multiSession: MultiSessionManager;
118
119
  gitWorktree: GitWorktreeManager;
119
120
  updater: AutoUpdater;
121
+ rateLimiter: RateLimitManager;
120
122
  private autoApproveSet;
121
123
  private requireApprovalSet;
122
124
  constructor(config: NeuroConfig);
@@ -137,7 +139,9 @@ export declare class NeuroEngine {
137
139
  */
138
140
  private initializeAgents;
139
141
  /**
140
- * Process a user message
142
+ * Process a user message — Claude Code style
143
+ * Every task is sent to the best agent which runs until completion.
144
+ * No arbitrary early termination — the agent keeps working until done.
141
145
  */
142
146
  processMessage(message: string, mode?: 'auto' | 'agent' | 'direct', targetAgent?: string): Promise<{
143
147
  content: string;
@@ -153,8 +157,9 @@ export declare class NeuroEngine {
153
157
  */
154
158
  private handleApproval;
155
159
  /**
156
- * Assess task complexity to decide execution mode
157
- * Now delegates to ModelRouter for more sophisticated analysis
160
+ * Assess task complexity delegates to ModelRouter
161
+ * Note: complexity no longer determines execution mode (all tasks run until complete)
162
+ * but is still used for model selection
158
163
  */
159
164
  private assessComplexity;
160
165
  /**