neoagent 2.4.1-beta.11 → 2.4.1-beta.13

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.
@@ -78,6 +78,11 @@ function buildSkipTaskAnalysisResult(forceMode) {
78
78
  draft_reply: '',
79
79
  goal: 'Complete the user request accurately.',
80
80
  success_criteria: [],
81
+ complexity: forceMode === 'plan_execute' ? 'complex' : 'standard',
82
+ autonomy_level: forceMode === 'plan_execute' ? 'high' : 'normal',
83
+ progress_update_policy: 'optional',
84
+ parallel_work: false,
85
+ completion_confidence_required: forceMode === 'plan_execute' ? 'high' : 'medium',
81
86
  };
82
87
  }
83
88
 
@@ -87,6 +92,11 @@ function buildAnalyzeTaskFallback(forceMode, userMessage = '') {
87
92
  verification_need: 'light',
88
93
  planning_depth: planningDepthForForceMode(forceMode),
89
94
  goal: userMessage ? String(userMessage).trim().slice(0, 300) : '',
95
+ complexity: forceMode === 'plan_execute' ? 'complex' : 'standard',
96
+ autonomy_level: forceMode === 'plan_execute' ? 'high' : 'normal',
97
+ progress_update_policy: 'optional',
98
+ parallel_work: false,
99
+ completion_confidence_required: forceMode === 'plan_execute' ? 'high' : 'medium',
90
100
  };
91
101
  }
92
102
 
@@ -97,6 +107,19 @@ function applyForcedAnalysisMode(analysis, forceMode) {
97
107
  ...analysis,
98
108
  mode: 'plan_execute',
99
109
  planning_depth: 'deep',
110
+ complexity: 'complex',
111
+ autonomy_level: 'high',
112
+ completion_confidence_required: analysis.completion_confidence_required || 'high',
113
+ };
114
+ }
115
+
116
+ function buildAutonomyPolicyFromAnalysis(analysis = {}) {
117
+ return {
118
+ complexity: analysis.complexity || 'standard',
119
+ autonomy_level: analysis.autonomy_level || 'normal',
120
+ progress_update_policy: analysis.progress_update_policy || 'optional',
121
+ parallel_work: analysis.parallel_work === true,
122
+ completion_confidence_required: analysis.completion_confidence_required || 'medium',
100
123
  };
101
124
  }
102
125
 
@@ -150,22 +173,19 @@ async function getProviderForUser(userId, task = '', isSubagent = false, modelOv
150
173
  if (userSelectedDefault && userSelectedDefault !== 'auto') {
151
174
  selectedModelDef = models.find((m) => m.id === userSelectedDefault) || fallbackModel;
152
175
  } else {
153
- const taskStr = String(task || '').toLowerCase();
154
-
155
- // Basic detection
156
- let isPlanning = /\b(plan|think|analy[sz]e|complex|step by step)\b/.test(taskStr);
157
- let isCoding = false;
158
-
159
- // Enhanced detection if enabled
160
- if (smarterSelection) {
161
- isPlanning = isPlanning || /\b(reason|strategy|logical|math|complex)\b/.test(taskStr);
162
- isCoding = /\b(code|program|script|debug|refactor|function|implementation|logic)\b/.test(taskStr);
163
- }
164
-
165
- if (isPlanning) {
176
+ const selectionHint = providerConfig.selectionHint && typeof providerConfig.selectionHint === 'object'
177
+ ? providerConfig.selectionHint
178
+ : {};
179
+ const preferredPurpose = String(selectionHint.purpose || '').trim().toLowerCase();
180
+ const highAutonomy = selectionHint.autonomyLevel === 'high' || selectionHint.complexity === 'complex';
181
+ const requestedPurpose = ['planning', 'coding', 'general', 'fast'].includes(preferredPurpose)
182
+ ? preferredPurpose
183
+ : '';
184
+
185
+ if (smarterSelection && requestedPurpose) {
186
+ selectedModelDef = availableModels.find((m) => m.purpose === requestedPurpose) || fallbackModel;
187
+ } else if (smarterSelection && highAutonomy) {
166
188
  selectedModelDef = availableModels.find((m) => m.purpose === 'planning') || fallbackModel;
167
- } else if (isCoding) {
168
- selectedModelDef = availableModels.find((m) => m.purpose === 'coding') || availableModels.find((m) => m.purpose === 'planning') || fallbackModel;
169
189
  } else if (isSubagent) {
170
190
  selectedModelDef = availableModels.find((m) => m.purpose === 'fast') || fallbackModel;
171
191
  } else {
@@ -411,6 +431,40 @@ function buildModelFailureLoopPrompt({ failedModel, nextModel, errorMessage }) {
411
431
  ].join(' ');
412
432
  }
413
433
 
434
+ function normalizeCompletionConfidence(value) {
435
+ const normalized = String(value || '').trim().toLowerCase();
436
+ if (normalized === 'high' || normalized === 'medium' || normalized === 'low') return normalized;
437
+ return 'medium';
438
+ }
439
+
440
+ function completionConfidenceRank(value) {
441
+ const normalized = normalizeCompletionConfidence(value);
442
+ if (normalized === 'high') return 3;
443
+ if (normalized === 'medium') return 2;
444
+ return 1;
445
+ }
446
+
447
+ function shouldAcceptTaskComplete({ confidence, requiredConfidence, iteration, maxIterations }) {
448
+ const required = normalizeCompletionConfidence(requiredConfidence || 'medium');
449
+ const actual = normalizeCompletionConfidence(confidence || 'medium');
450
+ if (completionConfidenceRank(actual) >= completionConfidenceRank(required)) {
451
+ return { accept: true, reason: '' };
452
+ }
453
+
454
+ const iterationsRemaining = Math.max(0, Number(maxIterations || 0) - Number(iteration || 0));
455
+ if (iterationsRemaining <= 1) {
456
+ return {
457
+ accept: true,
458
+ reason: `Accepted ${actual}-confidence completion at the iteration limit; final verifier will calibrate the answer.`,
459
+ };
460
+ }
461
+
462
+ return {
463
+ accept: false,
464
+ reason: `Completion confidence "${actual}" is below required "${required}". Continue with verification, recovery, or a narrower truthful result before completing.`,
465
+ };
466
+ }
467
+
414
468
  function clampRunContext(text, maxChars) {
415
469
  const value = normalizeOutgoingMessage(text);
416
470
  if (!value) return '';
@@ -1064,12 +1118,14 @@ class AgentEngine {
1064
1118
  '- A progress update is not complete.',
1065
1119
  '- A single failed tool attempt is not blocked if another safe retry, verification step, or alternative path remains.',
1066
1120
  '- A tool-specific API error, timeout, rate limit, or missing result inside this run is usually "continue", not "blocked", if any other available tool could still make progress.',
1121
+ '- If completion_confidence_required is high and the latest draft depends on unverified assumptions, use "continue" so the run can gather evidence, inspect state, or narrow the reply.',
1067
1122
  triggerSource === 'messaging' && messagingSent
1068
1123
  ? '- A reply was already delivered to the user via send_message. Use "complete" unless there is concrete remaining work (e.g., a tool call you still need to make) before the task is truly done. Do not send follow-up elaborations or re-introductions.'
1069
1124
  : triggerSource === 'messaging'
1070
1125
  ? '- For messaging, do not stop on a partial status message. Continue unless the task is actually complete or externally blocked. If you already asked for missing user input, choose "blocked" and wait.'
1071
1126
  : '- Do not stop just because you wrote a status update. Continue unless the task is actually complete or externally blocked.',
1072
1127
  analysis?.goal ? `Goal: ${analysis.goal}` : '',
1128
+ `Autonomy contract: complexity=${analysis?.complexity || 'standard'}; autonomy_level=${analysis?.autonomy_level || 'normal'}; progress_update_policy=${analysis?.progress_update_policy || 'optional'}; parallel_work=${analysis?.parallel_work === true}; completion_confidence_required=${analysis?.completion_confidence_required || 'medium'}.`,
1073
1129
  successCriteria.length > 0 ? `Success criteria:\n${successCriteria.map((item, index) => `${index + 1}. ${item}`).join('\n')}` : '',
1074
1130
  `Current iteration: ${iteration} of ${maxIterations}.`,
1075
1131
  `Available tools in this run: ${summarizeAvailableTools(tools) || 'none'}`,
@@ -1780,11 +1836,53 @@ class AgentEngine {
1780
1836
 
1781
1837
  }
1782
1838
 
1839
+ const activeDefaultModelSetting = triggerType === 'subagent'
1840
+ ? aiSettings.default_subagent_model
1841
+ : aiSettings.default_chat_model;
1842
+ if (!_modelOverride && activeDefaultModelSetting === 'auto' && aiSettings.smarter_model_selector !== false) {
1843
+ const requestedPurpose = analysis?.mode === 'plan_execute' || analysis?.complexity === 'complex' || analysis?.autonomy_level === 'high'
1844
+ ? 'planning'
1845
+ : triggerType === 'subagent'
1846
+ ? 'fast'
1847
+ : '';
1848
+ if (requestedPurpose) {
1849
+ const selectedAfterAnalysis = await getProviderForUser(
1850
+ userId,
1851
+ userMessage,
1852
+ triggerType === 'subagent',
1853
+ null,
1854
+ {
1855
+ ...providerStatusConfig,
1856
+ selectionHint: {
1857
+ purpose: requestedPurpose,
1858
+ complexity: analysis?.complexity,
1859
+ autonomyLevel: analysis?.autonomy_level,
1860
+ },
1861
+ }
1862
+ );
1863
+ if (selectedAfterAnalysis.model !== model) {
1864
+ provider = selectedAfterAnalysis.provider;
1865
+ model = selectedAfterAnalysis.model;
1866
+ providerName = selectedAfterAnalysis.providerName;
1867
+ db.prepare('UPDATE agent_runs SET model = ?, updated_at = datetime(\'now\') WHERE id = ?')
1868
+ .run(model, runId);
1869
+ this.emit(userId, 'run:interim', {
1870
+ runId,
1871
+ message: `Switched to ${model} for this run after task analysis.`,
1872
+ phase: 'model_selection'
1873
+ });
1874
+ }
1875
+ }
1876
+ }
1877
+
1783
1878
  // Rebuild loop policy with the resolved analysis mode. Runs in both the
1784
1879
  // normal path and the skipTaskAnalysis path so that forceMode='plan_execute'
1785
1880
  // (or any mode set by buildSkipTaskAnalysisResult) raises the iteration
1786
1881
  // ceiling correctly.
1787
- loopPolicy = buildLoopPolicy(aiSettings, triggerType, analysis.mode || 'execute', options);
1882
+ loopPolicy = buildLoopPolicy(aiSettings, triggerType, analysis.mode || 'execute', {
1883
+ ...options,
1884
+ autonomyPolicy: buildAutonomyPolicyFromAnalysis(analysis),
1885
+ });
1788
1886
  maxIterations = loopPolicy.maxIterations;
1789
1887
 
1790
1888
  if (options.skipDeliverableWorkflow !== true) {
@@ -2155,15 +2253,46 @@ class AgentEngine {
2155
2253
  // regular tool execution, it is a loop-exit signal.
2156
2254
  if (toolName === 'task_complete') {
2157
2255
  const finalMessage = String(toolArgs.message || '').trim();
2256
+ const confidence = normalizeCompletionConfidence(toolArgs.confidence || 'medium');
2257
+ const completionDecision = shouldAcceptTaskComplete({
2258
+ confidence,
2259
+ requiredConfidence: analysis?.completion_confidence_required || 'medium',
2260
+ iteration,
2261
+ maxIterations,
2262
+ });
2158
2263
  this.recordRunEvent(userId, runId, 'task_complete_signaled', {
2159
- confidence: toolArgs.confidence || 'high',
2264
+ confidence,
2265
+ requiredConfidence: analysis?.completion_confidence_required || 'medium',
2266
+ accepted: completionDecision.accept,
2160
2267
  iteration,
2161
2268
  messageLength: finalMessage.length,
2162
2269
  }, { agentId });
2163
2270
  console.info(
2164
- `[Run ${shortenRunId(runId)}] task_complete signaled at iteration=${iteration} confidence=${toolArgs.confidence || 'high'}`
2271
+ `[Run ${shortenRunId(runId)}] task_complete signaled at iteration=${iteration} confidence=${confidence} accepted=${completionDecision.accept}`
2165
2272
  );
2166
- // Always honor task_complete as a stop signal, even with no message.
2273
+ if (!completionDecision.accept) {
2274
+ messages.push({
2275
+ role: 'tool',
2276
+ name: toolName,
2277
+ tool_call_id: toolCall.id,
2278
+ content: JSON.stringify({
2279
+ status: 'continue',
2280
+ reason: completionDecision.reason,
2281
+ required_confidence: analysis?.completion_confidence_required || 'medium',
2282
+ }),
2283
+ });
2284
+ messages.push({
2285
+ role: 'system',
2286
+ content: `${completionDecision.reason} Do not ask the user to decide the next step unless external input is truly required.`
2287
+ });
2288
+ continue;
2289
+ }
2290
+ if (completionDecision.reason) {
2291
+ messages.push({
2292
+ role: 'system',
2293
+ content: completionDecision.reason,
2294
+ });
2295
+ }
2167
2296
  lastContent = finalMessage; // empty string is valid; downstream handles it
2168
2297
  directAnswerEligible = true;
2169
2298
  break; // exit the for-loop; the while condition will also exit
@@ -46,6 +46,13 @@ function clampFinite(n, lo, hi, fallback) {
46
46
  * @returns {LoopPolicy}
47
47
  */
48
48
  function buildLoopPolicy(aiSettings = {}, triggerType = 'chat', analysisMode = 'execute', options = {}) {
49
+ const autonomyPolicy = options.autonomyPolicy && typeof options.autonomyPolicy === 'object'
50
+ ? options.autonomyPolicy
51
+ : {};
52
+ const complexity = String(autonomyPolicy.complexity || '').trim().toLowerCase();
53
+ const autonomyLevel = String(autonomyPolicy.autonomy_level || autonomyPolicy.autonomyLevel || '').trim().toLowerCase();
54
+ const parallelWork = autonomyPolicy.parallel_work === true || autonomyPolicy.parallelWork === true;
55
+
49
56
  // ── maxIterations ────────────────────────────────────────────────────────
50
57
  // Resolve raw value from options → aiSettings → mode/context defaults,
51
58
  // then clamp to [1, MAX_ALLOWED_ITERATIONS] and floor to integer.
@@ -58,6 +65,10 @@ function buildLoopPolicy(aiSettings = {}, triggerType = 'chat', analysisMode = '
58
65
  rawIterations = DEFAULT_WIDGET_MAX_ITERATIONS;
59
66
  } else if (analysisMode === 'plan_execute') {
60
67
  rawIterations = DEFAULT_PLAN_EXECUTE_MAX_ITERATIONS;
68
+ } else if (complexity === 'complex' || autonomyLevel === 'high') {
69
+ rawIterations = DEFAULT_PLAN_EXECUTE_MAX_ITERATIONS;
70
+ } else if (parallelWork || complexity === 'standard') {
71
+ rawIterations = Math.max(DEFAULT_MAX_ITERATIONS, 28);
61
72
  } else {
62
73
  rawIterations = DEFAULT_MAX_ITERATIONS;
63
74
  }
@@ -6,6 +6,8 @@ const { OpenAIProvider } = require('./providers/openai');
6
6
  const { GithubCopilotProvider } = require('./providers/githubCopilot');
7
7
  const { OpenAICodexProvider } = require('./providers/openaiCodex');
8
8
  const { ClaudeCodeProvider } = require('./providers/claudeCode');
9
+ const { GrokOAuthProvider } = require('./providers/grokOauth');
10
+ const { NvidiaProvider } = require('./providers/nvidia');
9
11
  const {
10
12
  AI_PROVIDER_DEFINITIONS,
11
13
  getProviderConfigs,
@@ -19,6 +21,18 @@ const STATIC_MODELS = [
19
21
  provider: 'grok',
20
22
  purpose: 'general'
21
23
  },
24
+ {
25
+ id: 'grok-4',
26
+ label: 'Grok 4 (xAI OAuth / Default)',
27
+ provider: 'grok-oauth',
28
+ purpose: 'general'
29
+ },
30
+ {
31
+ id: 'grok-4-mini',
32
+ label: 'Grok 4 Mini (xAI OAuth / Fast)',
33
+ provider: 'grok-oauth',
34
+ purpose: 'coding'
35
+ },
22
36
  {
23
37
  id: 'gpt-5.3',
24
38
  label: 'GPT-5.3 (Copilot Default)',
@@ -49,6 +63,56 @@ const STATIC_MODELS = [
49
63
  provider: 'openai-codex',
50
64
  purpose: 'general'
51
65
  },
66
+ // — NVIDIA NIM — free tier ————————————————————————————————————————————
67
+ {
68
+ id: 'nvidia/nemotron-3-super-120b-a12b',
69
+ label: 'Nemotron Super 120B (NVIDIA / Free)',
70
+ provider: 'nvidia',
71
+ purpose: 'general'
72
+ },
73
+ {
74
+ id: 'moonshotai/kimi-k2.5',
75
+ label: 'Kimi K2.5 (NVIDIA NIM / Free)',
76
+ provider: 'nvidia',
77
+ purpose: 'coding'
78
+ },
79
+ {
80
+ id: 'minimaxai/minimax-m2.5',
81
+ label: 'MiniMax M2.5 (NVIDIA NIM / Free)',
82
+ provider: 'nvidia',
83
+ purpose: 'planning'
84
+ },
85
+ {
86
+ id: 'z-ai/glm5',
87
+ label: 'GLM 5 (NVIDIA NIM / Free)',
88
+ provider: 'nvidia',
89
+ purpose: 'fast'
90
+ },
91
+ // — NVIDIA NIM — notable models ————————————————————————————————————————
92
+ {
93
+ id: 'meta/llama-4-maverick-17b-128e-instruct',
94
+ label: 'Llama 4 Maverick 17B (NVIDIA NIM)',
95
+ provider: 'nvidia',
96
+ purpose: 'general'
97
+ },
98
+ {
99
+ id: 'meta/llama-4-scout-17b-16e-instruct',
100
+ label: 'Llama 4 Scout 17B (NVIDIA NIM)',
101
+ provider: 'nvidia',
102
+ purpose: 'planning'
103
+ },
104
+ {
105
+ id: 'deepseek-ai/deepseek-r1-0528',
106
+ label: 'DeepSeek R1 0528 (NVIDIA NIM)',
107
+ provider: 'nvidia',
108
+ purpose: 'general'
109
+ },
110
+ {
111
+ id: 'qwen/qwq-32b',
112
+ label: 'QwQ 32B (NVIDIA NIM)',
113
+ provider: 'nvidia',
114
+ purpose: 'planning'
115
+ },
52
116
  {
53
117
  id: 'gpt-5-nano',
54
118
  label: 'GPT-5 Nano (Fast / Subagents)',
@@ -359,6 +423,10 @@ function createProviderInstance(providerStr, userId = null, configOverrides = {}
359
423
  return new OpenAICodexProvider({ apiKey: runtime.apiKey, ...providerOverrides });
360
424
  } else if (providerStr === 'claude-code') {
361
425
  return new ClaudeCodeProvider({ apiKey: runtime.apiKey, ...providerOverrides });
426
+ } else if (providerStr === 'grok-oauth') {
427
+ return new GrokOAuthProvider({ apiKey: runtime.apiKey, ...providerOverrides });
428
+ } else if (providerStr === 'nvidia') {
429
+ return new NvidiaProvider({ apiKey: runtime.apiKey, baseUrl: runtime.baseUrl, ...providerOverrides });
362
430
  }
363
431
  throw new Error(`Unknown provider: ${providerStr}`);
364
432
  }
@@ -0,0 +1,141 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const OpenAI = require('openai');
4
+ const { GrokProvider } = require('./grok');
5
+
6
+ const GROK_OAUTH_BASE_URL = 'https://api.x.ai/v1';
7
+ const GROK_OAUTH_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828';
8
+ const GROK_OAUTH_TOKEN_URL = 'https://auth.x.ai/oauth2/token';
9
+ const GROK_OAUTH_SCOPES = 'openid profile email offline_access grok-cli:access api:access';
10
+
11
+ function normalizeExpiresAt(data) {
12
+ if (typeof data.expires_at === 'number' && Number.isFinite(data.expires_at)) {
13
+ return data.expires_at > 10_000_000_000 ? data.expires_at : data.expires_at * 1000;
14
+ }
15
+ if (typeof data.expires_in === 'number' && Number.isFinite(data.expires_in)) {
16
+ return Date.now() + (data.expires_in * 1000);
17
+ }
18
+ return null;
19
+ }
20
+
21
+ function persistEnvValue(key, value) {
22
+ if (!value) return;
23
+ try {
24
+ const { ENV_FILE } = require('../../../../runtime/paths');
25
+ const safeKey = String(key).replace(/[\r\n]/g, '');
26
+ const safeValue = String(value).replace(/[\r\n]/g, '');
27
+ const raw = fs.existsSync(ENV_FILE) ? fs.readFileSync(ENV_FILE, 'utf8') : '';
28
+ const lines = raw ? raw.split('\n') : [];
29
+ let replaced = false;
30
+ for (let i = 0; i < lines.length; i++) {
31
+ if (lines[i].startsWith(`${safeKey}=`)) {
32
+ lines[i] = `${safeKey}=${safeValue}`;
33
+ replaced = true;
34
+ break;
35
+ }
36
+ }
37
+ if (!replaced) lines.push(`${safeKey}=${safeValue}`);
38
+ const output = lines.filter((_, idx, arr) => idx !== arr.length - 1 || arr[idx] !== '').join('\n') + '\n';
39
+ fs.mkdirSync(path.dirname(ENV_FILE), { recursive: true });
40
+ fs.writeFileSync(ENV_FILE, output, { mode: 0o600 });
41
+ } catch { }
42
+ }
43
+
44
+ async function refreshGrokOAuthAccessToken(refreshToken, fetchImpl = fetch) {
45
+ if (!refreshToken) return null;
46
+ const response = await fetchImpl(GROK_OAUTH_TOKEN_URL, {
47
+ method: 'POST',
48
+ headers: {
49
+ 'Content-Type': 'application/x-www-form-urlencoded',
50
+ 'Accept': 'application/json',
51
+ },
52
+ body: new URLSearchParams({
53
+ grant_type: 'refresh_token',
54
+ refresh_token: refreshToken,
55
+ client_id: GROK_OAUTH_CLIENT_ID,
56
+ }),
57
+ });
58
+
59
+ const text = await response.text();
60
+ let data = {};
61
+ try {
62
+ data = text ? JSON.parse(text) : {};
63
+ } catch {
64
+ data = {};
65
+ }
66
+
67
+ if (!response.ok) {
68
+ if (data?.error === 'invalid_grant') {
69
+ throw new Error('Grok OAuth refresh token is invalid or expired. Re-run `neoagent login grok-oauth` to re-authenticate.');
70
+ }
71
+ const detail = data?.error_description || data?.error || text || 'Unknown error';
72
+ throw new Error(`Grok OAuth refresh failed: HTTP ${response.status} ${detail}`);
73
+ }
74
+ if (!data.access_token) {
75
+ throw new Error('Grok OAuth refresh succeeded but no access_token was returned.');
76
+ }
77
+
78
+ return {
79
+ access: data.access_token,
80
+ refresh: data.refresh_token || refreshToken,
81
+ expires: normalizeExpiresAt(data),
82
+ };
83
+ }
84
+
85
+ class GrokOAuthProvider extends GrokProvider {
86
+ constructor(config = {}) {
87
+ const authToken = config.apiKey || process.env.GROK_OAUTH_ACCESS_TOKEN;
88
+ super({
89
+ ...config,
90
+ apiKey: authToken,
91
+ baseUrl: GROK_OAUTH_BASE_URL,
92
+ });
93
+ this.name = 'grok-oauth';
94
+ this.models = ['grok-4', 'grok-4-mini'];
95
+
96
+ if (!authToken) {
97
+ console.warn('[GrokOAuth] No access token. Run `neoagent login grok-oauth` to authenticate.');
98
+ }
99
+
100
+ this.authToken = authToken || null;
101
+ this.refreshToken = config.refreshToken || process.env.GROK_OAUTH_REFRESH_TOKEN || null;
102
+ this.fetchImpl = config.fetch || fetch;
103
+ }
104
+
105
+ async refreshClient() {
106
+ const refreshed = await refreshGrokOAuthAccessToken(this.refreshToken, this.fetchImpl);
107
+ if (!refreshed?.access) return false;
108
+ this.authToken = refreshed.access;
109
+ this.refreshToken = refreshed.refresh || this.refreshToken;
110
+ process.env.GROK_OAUTH_ACCESS_TOKEN = this.authToken;
111
+ persistEnvValue('GROK_OAUTH_ACCESS_TOKEN', this.authToken);
112
+ if (this.refreshToken) {
113
+ process.env.GROK_OAUTH_REFRESH_TOKEN = this.refreshToken;
114
+ persistEnvValue('GROK_OAUTH_REFRESH_TOKEN', this.refreshToken);
115
+ }
116
+ this.client = new OpenAI({ apiKey: this.authToken, baseURL: GROK_OAUTH_BASE_URL });
117
+ return true;
118
+ }
119
+
120
+ async chat(messages, tools = [], options = {}) {
121
+ try {
122
+ return await super.chat(messages, tools, options);
123
+ } catch (err) {
124
+ if (err?.status !== 401 || !this.refreshToken) throw err;
125
+ await this.refreshClient();
126
+ return await super.chat(messages, tools, options);
127
+ }
128
+ }
129
+
130
+ async *stream(messages, tools = [], options = {}) {
131
+ try {
132
+ yield* super.stream(messages, tools, options);
133
+ } catch (err) {
134
+ if (err?.status !== 401 || !this.refreshToken) throw err;
135
+ await this.refreshClient();
136
+ yield* super.stream(messages, tools, options);
137
+ }
138
+ }
139
+ }
140
+
141
+ module.exports = { GrokOAuthProvider, refreshGrokOAuthAccessToken, GROK_OAUTH_SCOPES, GROK_OAUTH_CLIENT_ID };
@@ -0,0 +1,165 @@
1
+ const OpenAI = require('openai');
2
+ const { BaseProvider } = require('./base');
3
+
4
+ const NVIDIA_BASE_URL = 'https://integrate.api.nvidia.com/v1';
5
+
6
+ // Context windows per model (tokens)
7
+ const CONTEXT_WINDOWS = {
8
+ 'nvidia/nemotron-3-super-120b-a12b': 262144,
9
+ 'moonshotai/kimi-k2.5': 262144,
10
+ 'minimaxai/minimax-m2.5': 196608,
11
+ 'z-ai/glm5': 202752,
12
+ 'meta/llama-4-maverick-17b-128e-instruct': 1048576,
13
+ 'meta/llama-4-scout-17b-16e-instruct': 1048576,
14
+ 'deepseek-ai/deepseek-r1-0528': 163840,
15
+ 'qwen/qwq-32b': 131072,
16
+ };
17
+
18
+ // Reasoning models: no temperature, no top_p
19
+ const REASONING_MODELS = new Set([
20
+ 'deepseek-ai/deepseek-r1-0528',
21
+ 'qwen/qwq-32b',
22
+ ]);
23
+
24
+ class NvidiaProvider extends BaseProvider {
25
+ constructor(config = {}) {
26
+ super(config);
27
+ this.name = 'nvidia';
28
+ this.models = Object.keys(CONTEXT_WINDOWS);
29
+ this.client = new OpenAI({
30
+ apiKey: config.apiKey || process.env.NVIDIA_API_KEY,
31
+ baseURL: config.baseUrl || NVIDIA_BASE_URL,
32
+ });
33
+ }
34
+
35
+ getContextWindow(model) {
36
+ return CONTEXT_WINDOWS[model] ?? 131072;
37
+ }
38
+
39
+ _isReasoningModel(model) {
40
+ return REASONING_MODELS.has(model);
41
+ }
42
+
43
+ _buildParams(model, messages, tools, options) {
44
+ const params = {
45
+ model,
46
+ messages,
47
+ max_tokens: options.maxTokens || 8192,
48
+ };
49
+
50
+ if (!this._isReasoningModel(model)) {
51
+ params.temperature = options.temperature ?? 0.6;
52
+ }
53
+
54
+ if (tools && tools.length > 0) {
55
+ params.tools = this.formatTools(tools);
56
+ params.tool_choice = 'auto';
57
+ }
58
+
59
+ return params;
60
+ }
61
+
62
+ async chat(messages, tools = [], options = {}) {
63
+ const model = options.model || this.getDefaultModel();
64
+ const params = this._buildParams(model, messages, tools, options);
65
+ let response;
66
+ try {
67
+ response = await this.client.chat.completions.create(params);
68
+ } catch (err) {
69
+ throw new Error(`NVIDIA NIM request failed: ${err?.message || String(err)}`);
70
+ }
71
+ return this._normalizeResponse(response);
72
+ }
73
+
74
+ async *stream(messages, tools = [], options = {}) {
75
+ const model = options.model || this.getDefaultModel();
76
+ const params = {
77
+ ...this._buildParams(model, messages, tools, options),
78
+ stream: true,
79
+ stream_options: { include_usage: true },
80
+ };
81
+
82
+ let stream;
83
+ try {
84
+ stream = await this.client.chat.completions.create(params);
85
+ } catch (err) {
86
+ throw new Error(`NVIDIA NIM request failed: ${err?.message || String(err)}`);
87
+ }
88
+
89
+ let toolCalls = [];
90
+ let content = '';
91
+ let finalUsage = null;
92
+
93
+ for await (const chunk of stream) {
94
+ if (chunk.usage && (!chunk.choices || chunk.choices.length === 0)) {
95
+ finalUsage = this._normalizeUsage(chunk.usage);
96
+ continue;
97
+ }
98
+
99
+ const delta = chunk.choices?.[0]?.delta;
100
+ if (!delta) continue;
101
+
102
+ if (delta.content) {
103
+ content += delta.content;
104
+ yield { type: 'content', content: delta.content };
105
+ }
106
+
107
+ if (delta.tool_calls) {
108
+ for (const tc of delta.tool_calls) {
109
+ if (!toolCalls[tc.index]) {
110
+ toolCalls[tc.index] = {
111
+ id: tc.id || '',
112
+ type: 'function',
113
+ function: { name: tc.function?.name || '', arguments: '' },
114
+ };
115
+ }
116
+ if (tc.id) toolCalls[tc.index].id = tc.id;
117
+ if (tc.function?.name) toolCalls[tc.index].function.name = tc.function.name;
118
+ if (tc.function?.arguments) toolCalls[tc.index].function.arguments += tc.function.arguments;
119
+ }
120
+ }
121
+
122
+ const finishReason = chunk.choices[0]?.finish_reason;
123
+ if (finishReason === 'tool_calls' || (finishReason === 'stop' && toolCalls.length > 0)) {
124
+ yield { type: 'tool_calls', toolCalls, content, usage: this._normalizeUsage(chunk.usage) || finalUsage };
125
+ return;
126
+ }
127
+ if (finishReason === 'stop') {
128
+ yield { type: 'done', content, usage: this._normalizeUsage(chunk.usage) || finalUsage };
129
+ return;
130
+ }
131
+ }
132
+
133
+ if (toolCalls.length > 0) {
134
+ yield { type: 'tool_calls', toolCalls, content, usage: finalUsage };
135
+ } else {
136
+ yield { type: 'done', content, usage: finalUsage };
137
+ }
138
+ }
139
+
140
+ _normalizeResponse(response) {
141
+ const choice = response.choices[0];
142
+ const msg = choice.message;
143
+ return {
144
+ content: msg.content || '',
145
+ toolCalls: msg.tool_calls?.map((tc) => ({
146
+ id: tc.id,
147
+ type: 'function',
148
+ function: { name: tc.function.name, arguments: tc.function.arguments },
149
+ })) || [],
150
+ finishReason: choice.finish_reason,
151
+ usage: this._normalizeUsage(response.usage),
152
+ };
153
+ }
154
+
155
+ _normalizeUsage(usage) {
156
+ if (!usage) return null;
157
+ return {
158
+ promptTokens: usage.prompt_tokens ?? 0,
159
+ completionTokens: usage.completion_tokens ?? 0,
160
+ totalTokens: usage.total_tokens ?? 0,
161
+ };
162
+ }
163
+ }
164
+
165
+ module.exports = { NvidiaProvider };