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.
- package/flutter_app/lib/main.dart +1 -0
- package/flutter_app/lib/main_devices.dart +89 -2
- package/flutter_app/lib/main_model_picker.dart +631 -0
- package/flutter_app/lib/main_settings.dart +30 -86
- package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +217 -0
- package/lib/manager.js +131 -2
- package/package.json +1 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +58341 -58041
- package/server/services/ai/engine.js +148 -19
- package/server/services/ai/loopPolicy.js +11 -0
- package/server/services/ai/models.js +68 -0
- package/server/services/ai/providers/grokOauth.js +141 -0
- package/server/services/ai/providers/nvidia.js +165 -0
- package/server/services/ai/settings.js +20 -0
- package/server/services/ai/taskAnalysis.js +56 -0
- package/server/services/ai/tools.js +3 -3
- package/server/services/integrations/google/provider.js +13 -0
- package/server/services/integrations/manager.js +5 -0
- package/server/services/integrations/trello/provider.js +8 -2
|
@@ -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
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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',
|
|
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
|
|
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=${
|
|
2271
|
+
`[Run ${shortenRunId(runId)}] task_complete signaled at iteration=${iteration} confidence=${confidence} accepted=${completionDecision.accept}`
|
|
2165
2272
|
);
|
|
2166
|
-
|
|
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 };
|