neoagent 2.4.1-beta.9 → 2.4.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.
- package/.env.example +33 -3
- package/LICENSE +111 -56
- package/README.md +8 -3
- package/docs/configuration.md +8 -0
- package/docs/getting-started.md +9 -3
- package/docs/index.md +4 -0
- package/extensions/chrome-browser/background.mjs +36 -4
- package/extensions/chrome-browser/icons/icon128.png +0 -0
- package/extensions/chrome-browser/icons/icon16.png +0 -0
- package/extensions/chrome-browser/icons/icon48.png +0 -0
- package/extensions/chrome-browser/icons/logo.svg +39 -8
- package/extensions/chrome-browser/manifest.json +3 -3
- package/extensions/chrome-browser/popup.html +5 -1
- package/extensions/chrome-browser/popup.js +13 -0
- package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
- package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
- package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +3 -0
- package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
- package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
- package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
- package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
- package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
- package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
- package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
- package/flutter_app/lib/main.dart +2 -0
- package/flutter_app/lib/main_account_settings.dart +10 -34
- package/flutter_app/lib/main_admin.dart +14 -1
- package/flutter_app/lib/main_app_shell.dart +377 -340
- package/flutter_app/lib/main_chat.dart +707 -227
- package/flutter_app/lib/main_controller.dart +197 -42
- package/flutter_app/lib/main_devices.dart +436 -4
- package/flutter_app/lib/main_integrations.dart +255 -6
- package/flutter_app/lib/main_launcher.dart +1 -1
- package/flutter_app/lib/main_model_picker.dart +685 -0
- package/flutter_app/lib/main_models.dart +212 -0
- package/flutter_app/lib/main_navigation.dart +1 -9
- package/flutter_app/lib/main_operations.dart +1417 -614
- package/flutter_app/lib/main_settings.dart +673 -871
- package/flutter_app/lib/main_shared.dart +971 -443
- package/flutter_app/lib/main_spacing.dart +4 -4
- package/flutter_app/lib/main_theme.dart +22 -14
- package/flutter_app/lib/main_unified.dart +5 -72
- 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 +219 -0
- package/flutter_app/lib/src/backend_client.dart +46 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +30 -7
- package/flutter_app/lib/src/desktop_companion_io.dart +71 -1
- package/flutter_app/lib/src/security/password_strength.dart +84 -0
- package/flutter_app/lib/src/stream_renderer.dart +42 -3
- package/flutter_app/lib/src/theme/palette.dart +76 -34
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/pubspec.lock +2 -2
- package/flutter_app/pubspec.yaml +7 -1
- package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
- package/flutter_app/tool/generate_desktop_branding.py +154 -152
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +40 -9
- package/flutter_app/web/favicon_light.svg +43 -0
- package/flutter_app/web/icons/Icon-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/windows/runner/main.cpp +7 -1
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/lib/manager.js +445 -83
- package/package.json +17 -3
- package/runtime/paths.js +3 -1
- package/server/admin/access.js +198 -0
- package/server/admin/admin.css +268 -0
- package/server/admin/admin.js +348 -0
- package/server/admin/analytics.js +128 -0
- package/server/admin/index.html +1015 -0
- package/server/admin/login.html +290 -0
- package/server/admin/logo.svg +43 -0
- package/server/admin/sql.js +134 -0
- package/server/admin/users.js +147 -0
- package/server/config/origins.js +3 -1
- package/server/db/database.js +92 -0
- package/server/db/ftsQuery.js +27 -0
- package/server/http/routes.js +1 -0
- package/server/http/static.js +23 -6
- package/server/index.js +1 -1
- package/server/middleware/adminAuth.js +48 -0
- package/server/middleware/auth.js +1 -40
- package/server/public/.last_build_id +1 -1
- package/server/public/app_icon.png +0 -0
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_512.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
- package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
- package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
- package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/web/icons/Icon-192.png +0 -0
- package/server/public/favicon.png +0 -0
- package/server/public/favicon.svg +40 -9
- package/server/public/favicon_light.svg +43 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/icons/Icon-192-light.png +0 -0
- package/server/public/icons/Icon-192.png +0 -0
- package/server/public/icons/Icon-512-light.png +0 -0
- package/server/public/icons/Icon-512.png +0 -0
- package/server/public/icons/Icon-maskable-192-light.png +0 -0
- package/server/public/icons/Icon-maskable-192.png +0 -0
- package/server/public/icons/Icon-maskable-512-light.png +0 -0
- package/server/public/icons/Icon-maskable-512.png +0 -0
- package/server/public/main.dart.js +88537 -85936
- package/server/routes/admin.js +632 -0
- package/server/routes/agent_profiles.js +3 -0
- package/server/routes/agents.js +1 -1
- package/server/routes/auth.js +3 -1
- package/server/routes/mcp.js +29 -13
- package/server/routes/memory.js +23 -1
- package/server/routes/messaging.js +2 -0
- package/server/routes/screenHistory.js +14 -8
- package/server/routes/settings.js +10 -2
- package/server/routes/stream.js +1 -1
- package/server/routes/triggers.js +4 -4
- package/server/routes/voice_assistant.js +36 -1
- package/server/routes/workspace.js +86 -0
- package/server/services/account/admin_two_factor.js +132 -0
- package/server/services/account/password_policy.js +6 -1
- package/server/services/ai/completion.js +44 -0
- package/server/services/ai/engine.js +321 -500
- package/server/services/ai/imageAnalysis.js +9 -5
- package/server/services/ai/logFormat.js +46 -0
- package/server/services/ai/loopPolicy.js +11 -0
- package/server/services/ai/messagingFallback.js +228 -0
- package/server/services/ai/models.js +194 -55
- package/server/services/ai/providerRetry.js +169 -0
- package/server/services/ai/providers/anthropic.js +15 -4
- package/server/services/ai/providers/google.js +38 -11
- package/server/services/ai/providers/grok.js +19 -68
- package/server/services/ai/providers/grokOauth.js +141 -0
- package/server/services/ai/providers/nvidia.js +154 -0
- package/server/services/ai/providers/ollama.js +76 -39
- package/server/services/ai/providers/openai.js +20 -31
- package/server/services/ai/providers/openaiCodex.js +6 -2
- package/server/services/ai/providers/openaiCompatible.js +70 -0
- package/server/services/ai/providers/openrouter.js +162 -0
- package/server/services/ai/settings.js +30 -0
- package/server/services/ai/systemPrompt.js +51 -29
- package/server/services/ai/taskAnalysis.js +60 -1
- package/server/services/ai/toolEvidence.js +207 -0
- package/server/services/ai/toolSelector.js +8 -1
- package/server/services/ai/tools.js +78 -13
- package/server/services/browser/extension/registry.js +94 -9
- package/server/services/desktop/registry.js +104 -1
- package/server/services/desktop/screenRecorder.js +208 -98
- package/server/services/desktop/screen_recorder_support.js +46 -0
- package/server/services/integrations/google/provider.js +13 -0
- package/server/services/integrations/home_assistant/constants.js +88 -0
- package/server/services/integrations/home_assistant/network.js +207 -0
- package/server/services/integrations/home_assistant/provider.js +249 -0
- package/server/services/integrations/home_assistant/snapshot.js +98 -0
- package/server/services/integrations/home_assistant/tools.js +101 -0
- package/server/services/integrations/manager.js +5 -0
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/trello/provider.js +8 -2
- package/server/services/manager.js +51 -53
- package/server/services/mcp/client.js +208 -268
- package/server/services/mcp/client_support.js +172 -0
- package/server/services/mcp/recovery.js +116 -0
- package/server/services/mcp/tool_operations.js +123 -0
- package/server/services/memory/ingestion.js +185 -370
- package/server/services/memory/ingestion_coverage.js +129 -0
- package/server/services/memory/ingestion_documents.js +123 -0
- package/server/services/memory/ingestion_support.js +171 -0
- package/server/services/memory/intelligence.js +181 -0
- package/server/services/memory/manager.js +476 -32
- package/server/services/messaging/automation.js +71 -134
- package/server/services/messaging/formatting_guides.js +26 -1
- package/server/services/messaging/inbound_queue.js +111 -0
- package/server/services/messaging/manager.js +25 -8
- package/server/services/messaging/typing_keepalive.js +110 -0
- package/server/services/streaming/android-stream.js +293 -40
- package/server/services/tasks/integration_runtime.js +4 -1
- package/server/services/tasks/runtime.js +415 -56
- package/server/services/tasks/task_repository.js +48 -6
- package/server/services/websocket.js +8 -2
- package/server/services/widgets/service.js +17 -1
- package/server/services/workspace/manager.js +116 -0
- package/server/utils/logger.js +44 -6
- package/server/utils/security.js +3 -0
- package/server/utils/version.js +29 -19
- package/server/services/memory/openhuman_uplift.test.js +0 -98
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Centralized transient-error retry for AI provider calls.
|
|
4
|
+
//
|
|
5
|
+
// A transient blip (rate limit, provider overload, brief network failure) should
|
|
6
|
+
// retry the SAME model with a short backoff. Only after these retries are
|
|
7
|
+
// exhausted does the engine fall back to a different (often weaker) model. This
|
|
8
|
+
// keeps response quality high and avoids burning the fallback chain on errors a
|
|
9
|
+
// one-second wait would have resolved.
|
|
10
|
+
|
|
11
|
+
const DEFAULTS = {
|
|
12
|
+
maxAttempts: 3, // total attempts including the first
|
|
13
|
+
baseDelayMs: 500,
|
|
14
|
+
maxDelayMs: 8000,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// HTTP statuses worth retrying: request timeout, conflict, rate limit, and the
|
|
18
|
+
// 5xx family including Anthropic's 529 "overloaded" and common CDN edge codes.
|
|
19
|
+
const RETRYABLE_STATUS = new Set([408, 409, 425, 429, 500, 502, 503, 504, 520, 521, 522, 524, 529]);
|
|
20
|
+
|
|
21
|
+
// Low-level socket / DNS errors surfaced by Node and undici.
|
|
22
|
+
const RETRYABLE_CODES = new Set([
|
|
23
|
+
'ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'EAI_AGAIN', 'ENOTFOUND',
|
|
24
|
+
'ENETUNREACH', 'EHOSTUNREACH', 'EAGAIN',
|
|
25
|
+
'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_SOCKET', 'UND_ERR_HEADERS_TIMEOUT',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function readNumberEnv(name, fallback, { min = 0, max = Number.MAX_SAFE_INTEGER } = {}) {
|
|
29
|
+
const raw = process.env[name];
|
|
30
|
+
if (raw === undefined || raw === null || String(raw).trim() === '') return fallback;
|
|
31
|
+
const parsed = Number(raw);
|
|
32
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
33
|
+
return Math.min(max, Math.max(min, parsed));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveConfig(overrides = {}) {
|
|
37
|
+
return {
|
|
38
|
+
maxAttempts: overrides.maxAttempts
|
|
39
|
+
?? readNumberEnv('NEOAGENT_AI_RETRY_MAX_ATTEMPTS', DEFAULTS.maxAttempts, { min: 1, max: 8 }),
|
|
40
|
+
baseDelayMs: overrides.baseDelayMs
|
|
41
|
+
?? readNumberEnv('NEOAGENT_AI_RETRY_BASE_MS', DEFAULTS.baseDelayMs, { min: 0, max: 60000 }),
|
|
42
|
+
maxDelayMs: overrides.maxDelayMs
|
|
43
|
+
?? readNumberEnv('NEOAGENT_AI_RETRY_MAX_MS', DEFAULTS.maxDelayMs, { min: 0, max: 120000 }),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// SDKs disagree on where they put the HTTP status: OpenAI/Anthropic expose
|
|
48
|
+
// `.status`, raw http clients use `.statusCode`, and some nest it under
|
|
49
|
+
// `.response.status`. Check all of them.
|
|
50
|
+
function getStatus(err) {
|
|
51
|
+
if (!err || typeof err !== 'object') return null;
|
|
52
|
+
const candidates = [err.status, err.statusCode, err.response?.status, err.cause?.status];
|
|
53
|
+
for (const value of candidates) {
|
|
54
|
+
const num = Number(value);
|
|
55
|
+
if (Number.isFinite(num) && num >= 100 && num < 600) return num;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getErrorCode(err) {
|
|
61
|
+
if (!err || typeof err !== 'object') return null;
|
|
62
|
+
return err.code || err.errno || err.cause?.code || null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isTransientError(err) {
|
|
66
|
+
if (!err) return false;
|
|
67
|
+
|
|
68
|
+
const status = getStatus(err);
|
|
69
|
+
if (status !== null) return RETRYABLE_STATUS.has(status);
|
|
70
|
+
|
|
71
|
+
const code = getErrorCode(err);
|
|
72
|
+
if (code && RETRYABLE_CODES.has(String(code))) return true;
|
|
73
|
+
|
|
74
|
+
// SDK connection wrappers that don't carry a status or code.
|
|
75
|
+
const name = String(err.name || '');
|
|
76
|
+
if (name === 'APIConnectionError' || name === 'APIConnectionTimeoutError') return true;
|
|
77
|
+
|
|
78
|
+
const message = String(err.message || '').toLowerCase();
|
|
79
|
+
if (!message) return false;
|
|
80
|
+
return /\b(overloaded|rate limit|timed? ?out|timeout|temporarily unavailable|connection (?:reset|refused|error)|socket hang up|network (?:error|timeout)|service unavailable)\b/.test(message);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Honor a server-provided Retry-After when present; it is authoritative over our
|
|
84
|
+
// own backoff. Supports both delta-seconds and `retry-after-ms` style headers.
|
|
85
|
+
function retryAfterMs(err) {
|
|
86
|
+
if (!err || typeof err !== 'object') return null;
|
|
87
|
+
const headers = err.headers || err.response?.headers;
|
|
88
|
+
const read = (name) => {
|
|
89
|
+
if (!headers) return undefined;
|
|
90
|
+
if (typeof headers.get === 'function') return headers.get(name);
|
|
91
|
+
return headers[name] ?? headers[name.toLowerCase()];
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const ms = read('retry-after-ms');
|
|
95
|
+
if (ms !== undefined && ms !== null && String(ms).trim() !== '') {
|
|
96
|
+
const parsed = Number(ms);
|
|
97
|
+
if (Number.isFinite(parsed) && parsed >= 0) return parsed;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const after = read('retry-after');
|
|
101
|
+
if (after !== undefined && after !== null && String(after).trim() !== '') {
|
|
102
|
+
const seconds = Number(after);
|
|
103
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
|
|
104
|
+
const date = Date.parse(String(after));
|
|
105
|
+
if (Number.isFinite(date)) return Math.max(0, date - Date.now());
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Exponential backoff with equal-jitter: half the window is fixed, half random,
|
|
112
|
+
// which spreads retries out without ever collapsing the delay to zero.
|
|
113
|
+
function computeBackoffMs(attempt, baseDelayMs, maxDelayMs) {
|
|
114
|
+
const exp = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
|
|
115
|
+
return Math.round(exp / 2 + Math.random() * (exp / 2));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function delay(ms) {
|
|
119
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Run `fn` with transient-error retries.
|
|
124
|
+
*
|
|
125
|
+
* @param {(attempt: number) => Promise<any>} fn The provider call to attempt;
|
|
126
|
+
* invoked with the current 1-based attempt number.
|
|
127
|
+
* @param {object} [options]
|
|
128
|
+
* @param {(err: any) => boolean} [options.isRetryable] Override transient classification.
|
|
129
|
+
* @param {(info: {attempt:number, delayMs:number, error:any}) => void} [options.onRetry]
|
|
130
|
+
* Called before each wait so callers can surface progress to the user.
|
|
131
|
+
* @param {string} [options.label] Prefix for diagnostic logs.
|
|
132
|
+
*/
|
|
133
|
+
async function withProviderRetry(fn, options = {}) {
|
|
134
|
+
const { maxAttempts, baseDelayMs, maxDelayMs } = resolveConfig(options);
|
|
135
|
+
const isRetryable = typeof options.isRetryable === 'function' ? options.isRetryable : isTransientError;
|
|
136
|
+
const label = options.label || 'ProviderRetry';
|
|
137
|
+
|
|
138
|
+
let attempt = 0;
|
|
139
|
+
while (true) {
|
|
140
|
+
attempt += 1;
|
|
141
|
+
try {
|
|
142
|
+
return await fn(attempt);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
const exhausted = attempt >= maxAttempts;
|
|
145
|
+
if (exhausted || !isRetryable(err)) throw err;
|
|
146
|
+
|
|
147
|
+
const waitMs = retryAfterMs(err) ?? computeBackoffMs(attempt, baseDelayMs, maxDelayMs);
|
|
148
|
+
console.warn(
|
|
149
|
+
`[${label}] transient failure on attempt ${attempt}/${maxAttempts}; retrying in ${waitMs}ms: ${String(err?.message || err).slice(0, 200)}`
|
|
150
|
+
);
|
|
151
|
+
if (typeof options.onRetry === 'function') {
|
|
152
|
+
try {
|
|
153
|
+
options.onRetry({ attempt, delayMs: waitMs, error: err });
|
|
154
|
+
} catch { /* a misbehaving progress callback must not abort the retry */ }
|
|
155
|
+
}
|
|
156
|
+
await delay(waitMs);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = {
|
|
162
|
+
withProviderRetry,
|
|
163
|
+
isTransientError,
|
|
164
|
+
retryAfterMs,
|
|
165
|
+
computeBackoffMs,
|
|
166
|
+
resolveConfig,
|
|
167
|
+
RETRYABLE_STATUS,
|
|
168
|
+
RETRYABLE_CODES,
|
|
169
|
+
};
|
|
@@ -6,18 +6,24 @@ class AnthropicProvider extends BaseProvider {
|
|
|
6
6
|
super(config);
|
|
7
7
|
this.name = 'anthropic';
|
|
8
8
|
this.models = [
|
|
9
|
+
'claude-opus-4-8',
|
|
10
|
+
'claude-opus-4-7',
|
|
11
|
+
'claude-sonnet-4-6',
|
|
9
12
|
'claude-sonnet-4-20250514',
|
|
10
|
-
'claude-
|
|
13
|
+
'claude-haiku-4-5-20251001',
|
|
11
14
|
'claude-3-5-haiku-20241022',
|
|
15
|
+
'claude-3-5-sonnet-20241022',
|
|
12
16
|
'claude-3-opus-20240229',
|
|
13
|
-
'MiniMax-M2.7'
|
|
14
17
|
];
|
|
15
18
|
this.contextWindows = {
|
|
19
|
+
'claude-opus-4-8': 1000000,
|
|
20
|
+
'claude-opus-4-7': 200000,
|
|
21
|
+
'claude-sonnet-4-6': 200000,
|
|
16
22
|
'claude-sonnet-4-20250514': 200000,
|
|
17
|
-
'claude-
|
|
23
|
+
'claude-haiku-4-5-20251001': 200000,
|
|
18
24
|
'claude-3-5-haiku-20241022': 200000,
|
|
25
|
+
'claude-3-5-sonnet-20241022': 200000,
|
|
19
26
|
'claude-3-opus-20240229': 200000,
|
|
20
|
-
'MiniMax-M2.7': 204800
|
|
21
27
|
};
|
|
22
28
|
this.client = new Anthropic({
|
|
23
29
|
apiKey: config.apiKey || process.env.ANTHROPIC_API_KEY,
|
|
@@ -25,6 +31,11 @@ class AnthropicProvider extends BaseProvider {
|
|
|
25
31
|
});
|
|
26
32
|
}
|
|
27
33
|
|
|
34
|
+
async listModels() {
|
|
35
|
+
const res = await this.client.models.list({ limit: 100 });
|
|
36
|
+
return (res.data || []).map((m) => ({ id: m.id, name: m.display_name || m.id }));
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
getContextWindow(model) {
|
|
29
40
|
return this.contextWindows[model] || 200000;
|
|
30
41
|
}
|
|
@@ -6,20 +6,49 @@ class GoogleProvider extends BaseProvider {
|
|
|
6
6
|
super(config);
|
|
7
7
|
this.name = 'google';
|
|
8
8
|
this.models = [
|
|
9
|
+
'gemini-3.5-pro',
|
|
10
|
+
'gemini-3.5-flash',
|
|
11
|
+
'gemini-3.1-pro',
|
|
12
|
+
'gemini-3.1-flash-lite-preview',
|
|
13
|
+
'gemini-2.5-pro',
|
|
14
|
+
'gemini-2.5-flash',
|
|
9
15
|
'gemini-2.0-flash',
|
|
10
|
-
'gemini-2.0-pro',
|
|
11
16
|
'gemini-1.5-pro',
|
|
12
17
|
'gemini-1.5-flash',
|
|
13
|
-
'gemini-3.1-flash-lite-preview'
|
|
14
18
|
];
|
|
15
19
|
this.contextWindows = {
|
|
20
|
+
'gemini-3.5-pro': 2097152,
|
|
21
|
+
'gemini-3.5-flash': 1048576,
|
|
22
|
+
'gemini-3.1-pro': 2097152,
|
|
23
|
+
'gemini-3.1-flash-lite-preview': 1048576,
|
|
24
|
+
'gemini-2.5-pro': 1048576,
|
|
25
|
+
'gemini-2.5-flash': 1048576,
|
|
16
26
|
'gemini-2.0-flash': 1048576,
|
|
17
|
-
'gemini-2.0-pro': 2097152,
|
|
18
27
|
'gemini-1.5-pro': 2097152,
|
|
19
28
|
'gemini-1.5-flash': 1048576,
|
|
20
|
-
'gemini-3.1-flash-lite-preview': 1048576
|
|
21
29
|
};
|
|
22
|
-
this.
|
|
30
|
+
this.apiKey = config.apiKey || process.env.GOOGLE_AI_KEY;
|
|
31
|
+
this.genAI = new GoogleGenerativeAI(this.apiKey);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async listModels() {
|
|
35
|
+
const DROP = /tts|lyria|robotics|deep-research|antigravity|computer-use|-image(?!.*it)/i;
|
|
36
|
+
const res = await fetch(
|
|
37
|
+
`https://generativelanguage.googleapis.com/v1beta/models?key=${this.apiKey}&pageSize=200`
|
|
38
|
+
);
|
|
39
|
+
if (!res.ok) throw new Error(`Google models API returned ${res.status}`);
|
|
40
|
+
const { models = [] } = await res.json();
|
|
41
|
+
return models
|
|
42
|
+
.filter((m) => {
|
|
43
|
+
const id = m.name.replace('models/', '');
|
|
44
|
+
return (m.supportedGenerationMethods || []).includes('generateContent')
|
|
45
|
+
&& !DROP.test(id);
|
|
46
|
+
})
|
|
47
|
+
.map((m) => {
|
|
48
|
+
const id = m.name.replace('models/', '');
|
|
49
|
+
this.contextWindows[id] = m.inputTokenLimit || 1048576;
|
|
50
|
+
return { id, name: m.displayName || id };
|
|
51
|
+
});
|
|
23
52
|
}
|
|
24
53
|
|
|
25
54
|
getContextWindow(model) {
|
|
@@ -177,14 +206,12 @@ class GoogleProvider extends BaseProvider {
|
|
|
177
206
|
const toolCalls = [];
|
|
178
207
|
|
|
179
208
|
for await (const chunk of result.stream) {
|
|
180
|
-
const text = chunk.text();
|
|
181
|
-
if (text) {
|
|
182
|
-
content += text;
|
|
183
|
-
yield { type: 'content', content: text };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
209
|
for (const candidate of chunk.candidates || []) {
|
|
187
210
|
for (const part of candidate.content?.parts || []) {
|
|
211
|
+
if (part.text) {
|
|
212
|
+
content += part.text;
|
|
213
|
+
yield { type: 'content', content: part.text };
|
|
214
|
+
}
|
|
188
215
|
if (part.functionCall) {
|
|
189
216
|
toolCalls.push({
|
|
190
217
|
id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const OpenAI = require('openai');
|
|
2
|
-
const {
|
|
2
|
+
const { OpenAICompatibleProvider } = require('./openaiCompatible');
|
|
3
3
|
|
|
4
|
-
class GrokProvider extends
|
|
4
|
+
class GrokProvider extends OpenAICompatibleProvider {
|
|
5
5
|
constructor(config = {}) {
|
|
6
6
|
super(config);
|
|
7
7
|
this.name = 'grok';
|
|
@@ -11,8 +11,21 @@ class GrokProvider extends BaseProvider {
|
|
|
11
11
|
});
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
async listModels() {
|
|
15
|
+
try {
|
|
16
|
+
const res = await this.client.models.list();
|
|
17
|
+
const DROP = /imagine|diffus|embed|-tts/i;
|
|
18
|
+
return res.data
|
|
19
|
+
.filter((m) => !DROP.test(m.id))
|
|
20
|
+
.map((m) => ({ id: m.id, name: m.id }));
|
|
21
|
+
} catch (err) {
|
|
22
|
+
throw new Error(`Failed to list Grok models: ${err.message || String(err)}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
getContextWindow(model) {
|
|
15
|
-
|
|
27
|
+
if (model === 'grok-4.3') return 1000000;
|
|
28
|
+
return 131072;
|
|
16
29
|
}
|
|
17
30
|
|
|
18
31
|
supportsVision() {
|
|
@@ -68,7 +81,7 @@ class GrokProvider extends BaseProvider {
|
|
|
68
81
|
|
|
69
82
|
for await (const chunk of stream) {
|
|
70
83
|
if (chunk.usage && (!chunk.choices || chunk.choices.length === 0)) {
|
|
71
|
-
finalUsage = this
|
|
84
|
+
finalUsage = this.normalizeUsage(chunk.usage);
|
|
72
85
|
continue;
|
|
73
86
|
}
|
|
74
87
|
|
|
@@ -97,7 +110,7 @@ class GrokProvider extends BaseProvider {
|
|
|
97
110
|
type: 'tool_calls',
|
|
98
111
|
toolCalls,
|
|
99
112
|
content,
|
|
100
|
-
usage: this
|
|
113
|
+
usage: this.normalizeUsage(chunk.usage) || finalUsage
|
|
101
114
|
};
|
|
102
115
|
return;
|
|
103
116
|
}
|
|
@@ -105,7 +118,7 @@ class GrokProvider extends BaseProvider {
|
|
|
105
118
|
yield {
|
|
106
119
|
type: 'done',
|
|
107
120
|
content,
|
|
108
|
-
usage: this
|
|
121
|
+
usage: this.normalizeUsage(chunk.usage) || finalUsage
|
|
109
122
|
};
|
|
110
123
|
return;
|
|
111
124
|
}
|
|
@@ -118,68 +131,6 @@ class GrokProvider extends BaseProvider {
|
|
|
118
131
|
}
|
|
119
132
|
}
|
|
120
133
|
|
|
121
|
-
normalizeResponse(response) {
|
|
122
|
-
const choice = response.choices[0];
|
|
123
|
-
const msg = choice.message;
|
|
124
|
-
return {
|
|
125
|
-
content: msg.content || '',
|
|
126
|
-
toolCalls: msg.tool_calls?.map(tc => ({
|
|
127
|
-
id: tc.id,
|
|
128
|
-
type: 'function',
|
|
129
|
-
function: { name: tc.function.name, arguments: tc.function.arguments }
|
|
130
|
-
})) || [],
|
|
131
|
-
finishReason: choice.finish_reason,
|
|
132
|
-
usage: this.#normalizeUsage(response.usage)
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
#normalizeUsage(usage) {
|
|
137
|
-
if (!usage) {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
return {
|
|
141
|
-
promptTokens: usage.prompt_tokens ?? usage.promptTokens ?? 0,
|
|
142
|
-
completionTokens: usage.completion_tokens ?? usage.completionTokens ?? 0,
|
|
143
|
-
totalTokens: usage.total_tokens ?? usage.totalTokens ?? 0,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
formatTools(tools) {
|
|
148
|
-
return tools.map(tool => ({
|
|
149
|
-
type: 'function',
|
|
150
|
-
function: {
|
|
151
|
-
name: tool.name,
|
|
152
|
-
description: tool.description,
|
|
153
|
-
parameters: tool.parameters
|
|
154
|
-
}
|
|
155
|
-
}));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async analyzeImage(options = {}) {
|
|
159
|
-
const model = options.model || this.getDefaultVisionModel();
|
|
160
|
-
const b64 = BaseProvider.readImageAsBase64(options.imagePath);
|
|
161
|
-
const response = await this.client.chat.completions.create({
|
|
162
|
-
model,
|
|
163
|
-
max_tokens: options.maxTokens || 4096,
|
|
164
|
-
messages: [{
|
|
165
|
-
role: 'user',
|
|
166
|
-
content: [
|
|
167
|
-
{ type: 'text', text: options.question || 'Describe this image in detail.' },
|
|
168
|
-
{
|
|
169
|
-
type: 'image_url',
|
|
170
|
-
image_url: {
|
|
171
|
-
url: `data:${options.mimeType || 'image/jpeg'};base64,${b64}`
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
]
|
|
175
|
-
}]
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
content: response.choices[0]?.message?.content || '',
|
|
180
|
-
model: response.model || model,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
134
|
}
|
|
184
135
|
|
|
185
136
|
module.exports = { GrokProvider };
|
|
@@ -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 };
|