neoagent 2.4.1-beta.8 → 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 +45 -7
- 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 -2
- package/extensions/chrome-browser/popup.html +5 -1
- package/extensions/chrome-browser/popup.js +15 -2
- 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 +3 -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 +338 -46
- package/flutter_app/lib/main_devices.dart +851 -122
- 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 +764 -887
- 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 +79 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +56 -7
- package/flutter_app/lib/src/desktop_companion_io.dart +77 -1
- package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
- package/flutter_app/lib/src/security/password_strength.dart +84 -0
- package/flutter_app/lib/src/stream_renderer.dart +205 -35
- 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/AppDelegate.swift +44 -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 +88944 -85720
- 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/browser.js +14 -0
- package/server/routes/browser_extension.js +21 -4
- package/server/routes/desktop.js +10 -0
- 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 +14 -2
- package/server/routes/stream.js +12 -3
- 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 +80 -15
- package/server/services/android/controller.js +36 -1
- package/server/services/browser/controller.js +18 -0
- package/server/services/browser/extension/manifest.js +33 -0
- package/server/services/browser/extension/provider.js +12 -6
- package/server/services/browser/extension/registry.js +229 -18
- package/server/services/desktop/protocol.js +1 -0
- package/server/services/desktop/provider.js +4 -0
- 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 -47
- 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/runtime/manager.js +7 -3
- package/server/services/runtime/settings.js +17 -0
- package/server/services/streaming/android-stream.js +296 -32
- package/server/services/streaming/stream-hub.js +1 -1
- 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
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
|
|
6
6
|
const { getProviderForUser } = require('./engine');
|
|
7
7
|
const { createProviderInstance, getProviderCatalog } = require('./models');
|
|
8
|
+
const { withProviderRetry } = require('./providerRetry');
|
|
8
9
|
|
|
9
10
|
function resolveImageMimeType(imagePath, overrideMimeType = null) {
|
|
10
11
|
const normalized = String(overrideMimeType || '').trim().toLowerCase();
|
|
@@ -76,11 +77,14 @@ async function analyzeImageForUser({
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
try {
|
|
79
|
-
const response = await
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
const response = await withProviderRetry(
|
|
81
|
+
() => candidate.provider.analyzeImage({
|
|
82
|
+
imagePath,
|
|
83
|
+
mimeType: resolveImageMimeType(imagePath, mimeType),
|
|
84
|
+
question,
|
|
85
|
+
}),
|
|
86
|
+
{ label: `ImageAnalysis ${candidate.providerName}` },
|
|
87
|
+
);
|
|
84
88
|
return {
|
|
85
89
|
description: String(response.content || '').trim(),
|
|
86
90
|
model: response.model || null,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Small, dependency-free helpers for shaping values into safe log lines and
|
|
4
|
+
// for tolerant JSON parsing. Shared across the AI engine and its sibling
|
|
5
|
+
// modules so log formatting stays consistent and testable.
|
|
6
|
+
|
|
7
|
+
function shortenRunId(runId) {
|
|
8
|
+
const value = String(runId || '').trim();
|
|
9
|
+
if (!value) return 'unknown';
|
|
10
|
+
return value.length <= 8 ? value : value.slice(0, 8);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function summarizeForLog(value, maxChars = 220) {
|
|
14
|
+
if (value == null) return '';
|
|
15
|
+
|
|
16
|
+
let text = '';
|
|
17
|
+
if (typeof value === 'string') {
|
|
18
|
+
text = value;
|
|
19
|
+
} else {
|
|
20
|
+
try {
|
|
21
|
+
text = JSON.stringify(value);
|
|
22
|
+
} catch {
|
|
23
|
+
text = String(value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
28
|
+
if (normalized.length <= maxChars) return normalized;
|
|
29
|
+
return `${normalized.slice(0, maxChars)}...`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseMaybeJson(value, fallback = null) {
|
|
33
|
+
if (!value) return fallback;
|
|
34
|
+
if (typeof value === 'object') return value;
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(value);
|
|
37
|
+
} catch {
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
shortenRunId,
|
|
44
|
+
summarizeForLog,
|
|
45
|
+
parseMaybeJson,
|
|
46
|
+
};
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Deterministic, model-free helpers for shaping outgoing messages and for
|
|
4
|
+
// constructing honest fallback replies when a run fails or the model returns a
|
|
5
|
+
// blank message. Kept free of engine state so the behavior is pure and unit
|
|
6
|
+
// testable: every function derives its output from its arguments alone.
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
buildPlatformFormattingGuide,
|
|
10
|
+
normalizeOutgoingMessageForPlatform,
|
|
11
|
+
} = require('../messaging/formatting_guides');
|
|
12
|
+
const { summarizeForLog } = require('./logFormat');
|
|
13
|
+
|
|
14
|
+
function normalizeOutgoingMessage(content, platform = null, options = {}) {
|
|
15
|
+
const normalized = normalizeOutgoingMessageForPlatform(platform, content);
|
|
16
|
+
if (options.collapseWhitespace === false) {
|
|
17
|
+
return normalized;
|
|
18
|
+
}
|
|
19
|
+
return normalized.replace(/\s+/g, ' ').trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function clampRunContext(text, maxChars) {
|
|
23
|
+
const value = normalizeOutgoingMessage(text);
|
|
24
|
+
if (!value) return '';
|
|
25
|
+
if (value.length <= maxChars) return value;
|
|
26
|
+
return `${value.slice(0, maxChars)}...`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function joinSentMessages(messages = []) {
|
|
30
|
+
if (!Array.isArray(messages)) return '';
|
|
31
|
+
return messages
|
|
32
|
+
.map((message) => String(message || '').trim())
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.join('\n\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeInterimText(content, platform = null) {
|
|
38
|
+
return normalizeOutgoingMessageForPlatform(platform, content, {
|
|
39
|
+
stripNoResponseMarker: false,
|
|
40
|
+
}).trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildBlankMessagingReplyPrompt(attempt, platform = null) {
|
|
44
|
+
const formattingGuide = buildPlatformFormattingGuide(platform);
|
|
45
|
+
if (attempt <= 1) {
|
|
46
|
+
return `You must send one non-empty reply for the external messaging user right now. Do not call tools. Give either: (a) the concrete outcome, or (b) a clear blocker. If tool work already happened, summarize what you actually tried and where it got blocked. Do not ask the user to repeat the original request. Do not promise future work unless that work already happened in this run or will happen automatically before this reply is sent.\n\n${formattingGuide}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return `Your previous reply was empty. Return one non-empty message now. Do not call tools. If needed, apologize briefly and explain the blocker in one sentence. Use the run evidence already in the conversation instead of asking the user to restate the task. Do not promise future work unless that work already happened in this run or will happen automatically before this reply is sent.\n\n${formattingGuide}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseToolExecutionSummary(item) {
|
|
53
|
+
if (!item?.summary) return null;
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(item.summary);
|
|
56
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function toolWorkDescription(toolName) {
|
|
63
|
+
const name = String(toolName || '');
|
|
64
|
+
if (name === 'execute_command') return 'ran shell commands';
|
|
65
|
+
if (name === 'read_file' || name === 'search_files' || name === 'list_directory') return 'checked files';
|
|
66
|
+
if (name === 'web_search' || name === 'http_request') return 'looked up supporting information';
|
|
67
|
+
if (name.startsWith('browser_')) return 'checked the browser state';
|
|
68
|
+
if (name.startsWith('android_')) return 'checked the Android state';
|
|
69
|
+
if (name === 'read_health_data' || name.startsWith('recordings_')) return 'checked stored data';
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function summarizeRecentWork(toolExecutions = []) {
|
|
74
|
+
const descriptions = [];
|
|
75
|
+
for (const item of toolExecutions.slice(-6)) {
|
|
76
|
+
const description = toolWorkDescription(item?.toolName);
|
|
77
|
+
if (!description || descriptions.includes(description)) continue;
|
|
78
|
+
descriptions.push(description);
|
|
79
|
+
if (descriptions.length >= 2) break;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (descriptions.length === 0) return '';
|
|
83
|
+
if (descriptions.length === 1) return `I ${descriptions[0]}`;
|
|
84
|
+
return `I ${descriptions[0]} and ${descriptions[1]}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function hasFailureSignal(text) {
|
|
88
|
+
const normalized = normalizeOutgoingMessage(text);
|
|
89
|
+
if (!normalized) return false;
|
|
90
|
+
return /\b(error|failed|failure|traceback|exception|timed out|timeout|not found|no such file|permission denied|unable to|cannot|could not|module not found)\b/i.test(normalized);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function extractToolFailureMessage(item) {
|
|
94
|
+
const directError = normalizeOutgoingMessage(item?.error || '');
|
|
95
|
+
if (directError) return directError;
|
|
96
|
+
|
|
97
|
+
const summary = parseToolExecutionSummary(item);
|
|
98
|
+
if (!summary) return '';
|
|
99
|
+
|
|
100
|
+
const candidates = [
|
|
101
|
+
summary.message,
|
|
102
|
+
summary.note,
|
|
103
|
+
summary.stderr,
|
|
104
|
+
summary.stdout,
|
|
105
|
+
summary.content,
|
|
106
|
+
summary.excerpt,
|
|
107
|
+
summary.result,
|
|
108
|
+
summary.summary,
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
if (summary.status === 'error') {
|
|
112
|
+
for (const candidate of candidates) {
|
|
113
|
+
const normalized = normalizeOutgoingMessage(candidate || '');
|
|
114
|
+
if (normalized) return normalized;
|
|
115
|
+
}
|
|
116
|
+
if (summary.exitCode != null) {
|
|
117
|
+
return `The last shell command exited with code ${summary.exitCode}`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const candidate of candidates) {
|
|
122
|
+
const normalized = normalizeOutgoingMessage(candidate || '');
|
|
123
|
+
if (hasFailureSignal(normalized)) return normalized;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return '';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions = [] }) {
|
|
130
|
+
const workSummary = summarizeRecentWork(toolExecutions);
|
|
131
|
+
const blocker = [...toolExecutions].reverse()
|
|
132
|
+
.map((item) => extractToolFailureMessage(item))
|
|
133
|
+
.find(Boolean);
|
|
134
|
+
|
|
135
|
+
if (workSummary && blocker) {
|
|
136
|
+
return `${workSummary}, but I got blocked: ${blocker}. I do not have a confirmed finished result yet.`;
|
|
137
|
+
}
|
|
138
|
+
if (blocker) {
|
|
139
|
+
return `I got blocked while working on this: ${blocker}. I do not have a confirmed finished result yet.`;
|
|
140
|
+
}
|
|
141
|
+
if (workSummary && stepIndex > 0) {
|
|
142
|
+
return `${workSummary}, but I do not have a confirmed finished result yet.`;
|
|
143
|
+
}
|
|
144
|
+
if (failedStepCount > 0) {
|
|
145
|
+
return 'I ran into a tool problem while working on your request, so I do not have a confirmed finished result yet.';
|
|
146
|
+
}
|
|
147
|
+
if (stepIndex > 0) {
|
|
148
|
+
return 'I completed part of the work, but I do not have a confirmed finished result yet.';
|
|
149
|
+
}
|
|
150
|
+
return 'I could not produce a reliable final reply just now.';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildMessagingFailureScenario({ err, failedStepCount, stepIndex, toolExecutions = [] }) {
|
|
154
|
+
const parts = [];
|
|
155
|
+
const runtimeError = normalizeOutgoingMessage(err?.message || '');
|
|
156
|
+
const workSummary = summarizeRecentWork(toolExecutions);
|
|
157
|
+
const blocker = [...toolExecutions].reverse()
|
|
158
|
+
.map((item) => extractToolFailureMessage(item))
|
|
159
|
+
.find(Boolean);
|
|
160
|
+
|
|
161
|
+
if (runtimeError) {
|
|
162
|
+
parts.push(`Runtime error: ${summarizeForLog(runtimeError, 260)}.`);
|
|
163
|
+
}
|
|
164
|
+
if (workSummary) {
|
|
165
|
+
parts.push(`Observed work before failure: ${workSummary}.`);
|
|
166
|
+
}
|
|
167
|
+
if (blocker) {
|
|
168
|
+
parts.push(`Most specific blocker from run evidence: ${summarizeForLog(blocker, 260)}.`);
|
|
169
|
+
}
|
|
170
|
+
if (stepIndex > 0) {
|
|
171
|
+
parts.push(`Completed steps before failure: ${stepIndex}.`);
|
|
172
|
+
}
|
|
173
|
+
if (failedStepCount > 0) {
|
|
174
|
+
parts.push(`Failed tool steps: ${failedStepCount}.`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return parts.join(' ');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function buildDeterministicMessagingErrorReply({ err, failedStepCount, stepIndex, toolExecutions = [] }) {
|
|
181
|
+
const message = normalizeOutgoingMessage(err?.message || '');
|
|
182
|
+
if (/no ai providers? are currently available/i.test(message)) {
|
|
183
|
+
return 'I cannot continue right now because no AI provider is available for this account. Please check the provider settings.';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (/(timeout|timed out)/i.test(message)) {
|
|
187
|
+
return 'I hit a timeout while processing your request and could not finish it reliably.';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const blocker = [...toolExecutions].reverse()
|
|
191
|
+
.map((item) => extractToolFailureMessage(item))
|
|
192
|
+
.find(Boolean);
|
|
193
|
+
if (blocker) {
|
|
194
|
+
return `I got blocked while checking this: ${blocker}.`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (message) {
|
|
198
|
+
return `I got blocked while working on this: ${message}.`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return buildDeterministicMessagingFallback({ failedStepCount, stepIndex, toolExecutions });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function buildModelFailureLoopPrompt({ failedModel, nextModel, errorMessage }) {
|
|
205
|
+
return [
|
|
206
|
+
`The previous model call on "${failedModel}" failed with: ${summarizeForLog(errorMessage, 220)}.`,
|
|
207
|
+
`Continue on "${nextModel}" and recover autonomously.`,
|
|
208
|
+
'If a previous plan depended on that failed call, adjust your approach and proceed end-to-end.',
|
|
209
|
+
'Only ask the user for help if no safe path remains.'
|
|
210
|
+
].join(' ');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
normalizeOutgoingMessage,
|
|
215
|
+
clampRunContext,
|
|
216
|
+
joinSentMessages,
|
|
217
|
+
normalizeInterimText,
|
|
218
|
+
buildBlankMessagingReplyPrompt,
|
|
219
|
+
parseToolExecutionSummary,
|
|
220
|
+
toolWorkDescription,
|
|
221
|
+
summarizeRecentWork,
|
|
222
|
+
hasFailureSignal,
|
|
223
|
+
extractToolFailureMessage,
|
|
224
|
+
buildDeterministicMessagingFallback,
|
|
225
|
+
buildMessagingFailureScenario,
|
|
226
|
+
buildDeterministicMessagingErrorReply,
|
|
227
|
+
buildModelFailureLoopPrompt,
|
|
228
|
+
};
|
|
@@ -6,6 +6,9 @@ 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');
|
|
11
|
+
const { OpenRouterProvider } = require('./providers/openrouter');
|
|
9
12
|
const {
|
|
10
13
|
AI_PROVIDER_DEFINITIONS,
|
|
11
14
|
getProviderConfigs,
|
|
@@ -13,12 +16,21 @@ const {
|
|
|
13
16
|
} = require('./settings');
|
|
14
17
|
|
|
15
18
|
const STATIC_MODELS = [
|
|
19
|
+
// — xAI OAuth — fallback entries shown when grok-oauth token is invalid/exhausted.
|
|
20
|
+
// When the token is valid, DYNAMIC_PROVIDERS will replace these with the live list.
|
|
16
21
|
{
|
|
17
|
-
id: 'grok-4
|
|
18
|
-
label: 'Grok 4
|
|
19
|
-
provider: 'grok',
|
|
20
|
-
purpose: 'general'
|
|
22
|
+
id: 'grok-4',
|
|
23
|
+
label: 'Grok 4 (xAI OAuth)',
|
|
24
|
+
provider: 'grok-oauth',
|
|
25
|
+
purpose: 'general',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'grok-4-mini',
|
|
29
|
+
label: 'Grok 4 Mini (xAI OAuth)',
|
|
30
|
+
provider: 'grok-oauth',
|
|
31
|
+
purpose: 'fast',
|
|
21
32
|
},
|
|
33
|
+
// — GitHub Copilot (subscription; no public /models endpoint) ————————
|
|
22
34
|
{
|
|
23
35
|
id: 'gpt-5.3',
|
|
24
36
|
label: 'GPT-5.3 (Copilot Default)',
|
|
@@ -31,6 +43,7 @@ const STATIC_MODELS = [
|
|
|
31
43
|
provider: 'github-copilot',
|
|
32
44
|
purpose: 'coding'
|
|
33
45
|
},
|
|
46
|
+
// — OpenAI Codex ——————————————————————————————————————————————————————
|
|
34
47
|
{
|
|
35
48
|
id: 'gpt-5.5',
|
|
36
49
|
label: 'GPT-5.5 (Codex Default)',
|
|
@@ -49,29 +62,12 @@ const STATIC_MODELS = [
|
|
|
49
62
|
provider: 'openai-codex',
|
|
50
63
|
purpose: 'general'
|
|
51
64
|
},
|
|
65
|
+
// — Claude Code ————————————————————————————————————————————————————————
|
|
52
66
|
{
|
|
53
|
-
id: '
|
|
54
|
-
label: '
|
|
55
|
-
provider: '
|
|
56
|
-
purpose: '
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
id: 'gpt-5-mini',
|
|
60
|
-
label: 'GPT-5 Mini (Planning / Complex)',
|
|
61
|
-
provider: 'openai',
|
|
62
|
-
purpose: 'planning'
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
id: 'claude-sonnet-4-20250514',
|
|
66
|
-
label: 'Claude Sonnet 4 (Analysis / Writing)',
|
|
67
|
-
provider: 'anthropic',
|
|
68
|
-
purpose: 'planning'
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
id: 'claude-3-5-haiku-20241022',
|
|
72
|
-
label: 'Claude 3.5 Haiku (Fast)',
|
|
73
|
-
provider: 'anthropic',
|
|
74
|
-
purpose: 'fast'
|
|
67
|
+
id: 'claude-opus-4-8',
|
|
68
|
+
label: 'Claude Opus 4.8 (Claude Code / Flagship)',
|
|
69
|
+
provider: 'claude-code',
|
|
70
|
+
purpose: 'general'
|
|
75
71
|
},
|
|
76
72
|
{
|
|
77
73
|
id: 'claude-opus-4-7',
|
|
@@ -91,29 +87,155 @@ const STATIC_MODELS = [
|
|
|
91
87
|
provider: 'claude-code',
|
|
92
88
|
purpose: 'fast'
|
|
93
89
|
},
|
|
94
|
-
|
|
95
|
-
id: 'gemini-3.1-flash-lite-preview',
|
|
96
|
-
label: 'Gemini 3.1 Flash Lite (Preview)',
|
|
97
|
-
provider: 'google',
|
|
98
|
-
purpose: 'general'
|
|
99
|
-
},
|
|
90
|
+
// — MiniMax ————————————————————————————————————————————————————————————
|
|
100
91
|
{
|
|
101
92
|
id: 'MiniMax-M2.7',
|
|
102
93
|
label: 'MiniMax M2.7 (Coding Plan)',
|
|
103
94
|
provider: 'minimax',
|
|
104
95
|
purpose: 'coding'
|
|
105
96
|
},
|
|
97
|
+
// — Ollama — default suggestions (full list loaded dynamically) ————————
|
|
106
98
|
{
|
|
107
99
|
id: 'qwen3.5:4b',
|
|
108
100
|
label: 'Qwen 3.5 4B (Local / Ollama)',
|
|
109
101
|
provider: 'ollama',
|
|
110
|
-
purpose: 'general'
|
|
102
|
+
purpose: 'general',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'gemma4:12b',
|
|
106
|
+
label: 'Gemma 4 12B (Local / Ollama)',
|
|
107
|
+
provider: 'ollama',
|
|
108
|
+
purpose: 'general',
|
|
111
109
|
}
|
|
112
110
|
];
|
|
113
111
|
|
|
112
|
+
// Maps a provider id to its class and which runtime fields its constructor takes.
|
|
113
|
+
// Adding a provider is a one-line entry here instead of another dispatch branch.
|
|
114
|
+
// `apiKey`/`baseUrl` mirror exactly what each constructor was historically given;
|
|
115
|
+
// they intentionally do not derive from AI_PROVIDER_DEFINITIONS.supportsBaseUrl,
|
|
116
|
+
// which disagrees for github-copilot/openai-codex (those read their base URL from
|
|
117
|
+
// env, not from per-user config).
|
|
118
|
+
const PROVIDER_FACTORIES = Object.freeze({
|
|
119
|
+
grok: { Provider: GrokProvider, apiKey: true, baseUrl: true },
|
|
120
|
+
openai: { Provider: OpenAIProvider, apiKey: true, baseUrl: true },
|
|
121
|
+
anthropic: { Provider: AnthropicProvider, apiKey: true, baseUrl: true },
|
|
122
|
+
google: { Provider: GoogleProvider, apiKey: true, baseUrl: false },
|
|
123
|
+
minimax: { Provider: AnthropicProvider, apiKey: true, baseUrl: true },
|
|
124
|
+
ollama: { Provider: OllamaProvider, apiKey: false, baseUrl: true },
|
|
125
|
+
'github-copilot': { Provider: GithubCopilotProvider, apiKey: true, baseUrl: false },
|
|
126
|
+
'openai-codex': { Provider: OpenAICodexProvider, apiKey: true, baseUrl: false },
|
|
127
|
+
'claude-code': { Provider: ClaudeCodeProvider, apiKey: true, baseUrl: false },
|
|
128
|
+
'grok-oauth': { Provider: GrokOAuthProvider, apiKey: true, baseUrl: false },
|
|
129
|
+
nvidia: { Provider: NvidiaProvider, apiKey: true, baseUrl: true },
|
|
130
|
+
openrouter: { Provider: OpenRouterProvider, apiKey: true, baseUrl: true },
|
|
131
|
+
});
|
|
132
|
+
|
|
114
133
|
const dynamicModelsByBaseUrl = new Map();
|
|
115
134
|
const REFRESH_INTERVAL = 30000; // 30 seconds
|
|
116
135
|
|
|
136
|
+
// Unified dynamic model cache for all API-backed providers.
|
|
137
|
+
// Keyed by `${providerId}:${apiKey.slice(0,8)}` to handle per-user keys.
|
|
138
|
+
const providerModelCache = new Map();
|
|
139
|
+
const DYNAMIC_REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
|
140
|
+
|
|
141
|
+
// Populated from OpenRouter's /models response; used to price-classify models
|
|
142
|
+
// from all providers. Keyed by both the full OpenRouter ID ("openai/gpt-5-mini")
|
|
143
|
+
// and the bare model ID ("gpt-5-mini") for cross-provider lookup.
|
|
144
|
+
const openrouterPricingCache = new Map();
|
|
145
|
+
|
|
146
|
+
// Providers whose full model list is fetched from their API at runtime.
|
|
147
|
+
// grok-oauth inherits listModels() from GrokProvider and uses the same xAI endpoint.
|
|
148
|
+
const DYNAMIC_PROVIDERS = ['openai', 'anthropic', 'google', 'nvidia', 'grok', 'grok-oauth', 'openrouter'];
|
|
149
|
+
|
|
150
|
+
function inferModelPurpose(id) {
|
|
151
|
+
const s = id.toLowerCase();
|
|
152
|
+
if (/flash|nano|lite|tiny|haiku|scout|mini(?!max)|small/.test(s)) return 'fast';
|
|
153
|
+
if (/r1|qwq|o[0-9]|reasoning|thinking/.test(s)) return 'planning';
|
|
154
|
+
if (/code|coder|starcoder|devstral|codex|codegemma/.test(s)) return 'coding';
|
|
155
|
+
return 'general';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Pricing tiers: free=$0 cheap<$0.50/1M medium=$0.50–$5/1M expensive>$5/1M
|
|
159
|
+
// Uses live prices from openrouterPricingCache; returns null when unknown.
|
|
160
|
+
function classifyPriceTier(modelId) {
|
|
161
|
+
const costPerM = openrouterPricingCache.get(modelId);
|
|
162
|
+
if (costPerM === undefined) return null;
|
|
163
|
+
if (costPerM === 0) return 'free';
|
|
164
|
+
if (costPerM < 0.5) return 'cheap';
|
|
165
|
+
if (costPerM < 5) return 'medium';
|
|
166
|
+
return 'expensive';
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Per-provider functions that turn a raw model object from listModels() into a display label.
|
|
170
|
+
const PROVIDER_LABEL_FN = {
|
|
171
|
+
openai: (m) => `${m.id} (OpenAI)`,
|
|
172
|
+
anthropic: (m) => `${m.name || m.id} (Anthropic)`,
|
|
173
|
+
google: (m) => `${m.name || m.id} (Google)`,
|
|
174
|
+
nvidia: (m) => `${m.id} (NVIDIA NIM)`,
|
|
175
|
+
grok: (m) => `${m.id} (xAI)`,
|
|
176
|
+
openrouter:(m) => `${m.name || m.id} (OpenRouter)`,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
async function refreshProviderModelList(providerId, apiKey, baseUrl) {
|
|
180
|
+
const cacheKey = `${providerId}:${(apiKey || '').slice(0, 8)}`;
|
|
181
|
+
const existing = providerModelCache.get(cacheKey);
|
|
182
|
+
const now = Date.now();
|
|
183
|
+
|
|
184
|
+
if (existing && now - existing.lastRefresh <= DYNAMIC_REFRESH_INTERVAL) {
|
|
185
|
+
return existing.models;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const factory = PROVIDER_FACTORIES[providerId];
|
|
190
|
+
const config = {};
|
|
191
|
+
if (factory.apiKey) config.apiKey = apiKey;
|
|
192
|
+
if (factory.baseUrl) config.baseUrl = baseUrl;
|
|
193
|
+
const provider = new factory.Provider(config);
|
|
194
|
+
|
|
195
|
+
const raw = await provider.listModels();
|
|
196
|
+
|
|
197
|
+
// OpenRouter returns live pricing — populate the shared cache so all
|
|
198
|
+
// other providers can resolve their price tier without a lookup table.
|
|
199
|
+
if (providerId === 'openrouter') {
|
|
200
|
+
for (const m of raw) {
|
|
201
|
+
if (m.pricing?.prompt == null) continue;
|
|
202
|
+
const inputPerM = parseFloat(m.pricing.prompt) * 1_000_000;
|
|
203
|
+
openrouterPricingCache.set(m.id, inputPerM);
|
|
204
|
+
// Also index by the bare model ID (everything after the first "/")
|
|
205
|
+
// so that e.g. "gpt-5-mini" resolves from "openai/gpt-5-mini".
|
|
206
|
+
if (m.id.includes('/')) {
|
|
207
|
+
const bareId = m.id.slice(m.id.indexOf('/') + 1);
|
|
208
|
+
if (!openrouterPricingCache.has(bareId)) {
|
|
209
|
+
openrouterPricingCache.set(bareId, inputPerM);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const labelFn = PROVIDER_LABEL_FN[providerId] || ((m) => m.id);
|
|
216
|
+
const models = raw.map((m) => ({
|
|
217
|
+
id: m.id,
|
|
218
|
+
label: labelFn(m),
|
|
219
|
+
provider: providerId,
|
|
220
|
+
purpose: inferModelPurpose(m.id),
|
|
221
|
+
}));
|
|
222
|
+
|
|
223
|
+
providerModelCache.set(cacheKey, { models, lastRefresh: now });
|
|
224
|
+
return models;
|
|
225
|
+
} catch (err) {
|
|
226
|
+
console.warn(`[Models] Failed to refresh ${providerId} models:`, err.message);
|
|
227
|
+
// Always record a lastRefresh so we don't hammer the API on every request.
|
|
228
|
+
// Permanent errors (auth/billing/credits) get a longer backoff.
|
|
229
|
+
const isPermanent = /401|403|unauthorized|forbidden|credits|spending/i.test(err.message);
|
|
230
|
+
const backoff = isPermanent ? 30 * 60 * 1000 : DYNAMIC_REFRESH_INTERVAL;
|
|
231
|
+
providerModelCache.set(cacheKey, {
|
|
232
|
+
models: existing?.models || [],
|
|
233
|
+
lastRefresh: now - DYNAMIC_REFRESH_INTERVAL + backoff,
|
|
234
|
+
});
|
|
235
|
+
return existing?.models || [];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
117
239
|
async function probeOllama(baseUrl, timeoutMs = 1500) {
|
|
118
240
|
const controller = new AbortController();
|
|
119
241
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -277,8 +399,9 @@ async function getSupportedModels(userId, agentId = null) {
|
|
|
277
399
|
|
|
278
400
|
const all = [...STATIC_MODELS];
|
|
279
401
|
const staticIds = new Set(STATIC_MODELS.map((model) => model.id));
|
|
280
|
-
const ollama = providerById.get('ollama');
|
|
281
402
|
|
|
403
|
+
// Ollama: dynamic list from local server
|
|
404
|
+
const ollama = providerById.get('ollama');
|
|
282
405
|
if (ollama?.enabled) {
|
|
283
406
|
const dynamicModels = await refreshDynamicModels(ollama.baseUrl);
|
|
284
407
|
for (const model of dynamicModels) {
|
|
@@ -288,10 +411,35 @@ async function getSupportedModels(userId, agentId = null) {
|
|
|
288
411
|
}
|
|
289
412
|
}
|
|
290
413
|
|
|
414
|
+
// API-backed providers: fetch model lists in parallel
|
|
415
|
+
const dynamicFetches = DYNAMIC_PROVIDERS
|
|
416
|
+
.filter((id) => providerById.get(id)?.available)
|
|
417
|
+
.map(async (id) => {
|
|
418
|
+
const runtime = getProviderRuntimeConfig(userId, id, agentId);
|
|
419
|
+
return refreshProviderModelList(id, runtime.apiKey, runtime.baseUrl);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const dynamicResults = await Promise.allSettled(dynamicFetches);
|
|
423
|
+
for (const result of dynamicResults) {
|
|
424
|
+
if (result.status === 'fulfilled') {
|
|
425
|
+
for (const model of result.value) {
|
|
426
|
+
if (!staticIds.has(model.id)) {
|
|
427
|
+
all.push(model);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
291
433
|
return all.map((model) => {
|
|
292
434
|
const provider = providerById.get(model.provider);
|
|
435
|
+
// Ollama models are always local/free; all others look up the OpenRouter
|
|
436
|
+
// pricing cache (populated above by Promise.allSettled).
|
|
437
|
+
const priceTier = model.provider === 'ollama'
|
|
438
|
+
? 'free'
|
|
439
|
+
: (model.priceTier ?? classifyPriceTier(model.id));
|
|
293
440
|
return {
|
|
294
441
|
...model,
|
|
442
|
+
priceTier,
|
|
295
443
|
available: provider?.available !== false,
|
|
296
444
|
providerStatus: provider?.status || 'unknown',
|
|
297
445
|
providerStatusLabel: provider?.statusLabel || 'Unknown'
|
|
@@ -315,7 +463,7 @@ async function refreshDynamicModels(baseUrl) {
|
|
|
315
463
|
id: name,
|
|
316
464
|
label: `${name} (Ollama / Local)`,
|
|
317
465
|
provider: 'ollama',
|
|
318
|
-
purpose: 'general'
|
|
466
|
+
purpose: 'general',
|
|
319
467
|
}));
|
|
320
468
|
|
|
321
469
|
dynamicModelsByBaseUrl.set(cacheKey, {
|
|
@@ -331,6 +479,11 @@ async function refreshDynamicModels(baseUrl) {
|
|
|
331
479
|
}
|
|
332
480
|
|
|
333
481
|
function createProviderInstance(providerStr, userId = null, configOverrides = {}) {
|
|
482
|
+
const factory = PROVIDER_FACTORIES[providerStr];
|
|
483
|
+
if (!factory) {
|
|
484
|
+
throw new Error(`Unknown provider: ${providerStr}`);
|
|
485
|
+
}
|
|
486
|
+
|
|
334
487
|
const { agentId = null, ...providerOverrides } = configOverrides || {};
|
|
335
488
|
const runtime = getProviderRuntimeConfig(userId, providerStr, agentId);
|
|
336
489
|
|
|
@@ -341,30 +494,16 @@ function createProviderInstance(providerStr, userId = null, configOverrides = {}
|
|
|
341
494
|
throw new Error(`Provider '${providerStr}' is not configured on this deployment.`);
|
|
342
495
|
}
|
|
343
496
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
return new AnthropicProvider({ apiKey: runtime.apiKey, baseUrl: runtime.baseUrl, ...providerOverrides });
|
|
350
|
-
} else if (providerStr === 'google') {
|
|
351
|
-
return new GoogleProvider({ apiKey: runtime.apiKey, ...providerOverrides });
|
|
352
|
-
} else if (providerStr === 'minimax') {
|
|
353
|
-
return new AnthropicProvider({ apiKey: runtime.apiKey, baseUrl: runtime.baseUrl, ...providerOverrides });
|
|
354
|
-
} else if (providerStr === 'ollama') {
|
|
355
|
-
return new OllamaProvider({ baseUrl: runtime.baseUrl, ...providerOverrides });
|
|
356
|
-
} else if (providerStr === 'github-copilot') {
|
|
357
|
-
return new GithubCopilotProvider({ apiKey: runtime.apiKey, ...providerOverrides });
|
|
358
|
-
} else if (providerStr === 'openai-codex') {
|
|
359
|
-
return new OpenAICodexProvider({ apiKey: runtime.apiKey, ...providerOverrides });
|
|
360
|
-
} else if (providerStr === 'claude-code') {
|
|
361
|
-
return new ClaudeCodeProvider({ apiKey: runtime.apiKey, ...providerOverrides });
|
|
362
|
-
}
|
|
363
|
-
throw new Error(`Unknown provider: ${providerStr}`);
|
|
497
|
+
const config = {};
|
|
498
|
+
if (factory.apiKey) config.apiKey = runtime.apiKey;
|
|
499
|
+
if (factory.baseUrl) config.baseUrl = runtime.baseUrl;
|
|
500
|
+
|
|
501
|
+
return new factory.Provider({ ...config, ...providerOverrides });
|
|
364
502
|
}
|
|
365
503
|
|
|
366
504
|
module.exports = {
|
|
367
505
|
AI_PROVIDER_DEFINITIONS,
|
|
506
|
+
PROVIDER_FACTORIES,
|
|
368
507
|
SUPPORTED_MODELS: STATIC_MODELS, // Backward compatibility
|
|
369
508
|
createProviderInstance,
|
|
370
509
|
getProviderCatalog,
|