neoagent 3.2.1-beta.2 → 3.2.1-beta.3
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/extensions/chrome-browser/background.mjs +318 -88
- package/extensions/chrome-browser/http.mjs +136 -0
- package/extensions/chrome-browser/protocol.mjs +511 -89
- package/flutter_app/lib/main_chat.dart +118 -739
- package/flutter_app/lib/main_controller.dart +29 -20
- package/flutter_app/lib/main_models.dart +3 -0
- package/flutter_app/lib/main_operations.dart +334 -321
- package/flutter_app/lib/main_settings.dart +4 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
- package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
- package/flutter_app/windows/runner/flutter_window.cpp +143 -32
- package/lib/manager.js +106 -89
- package/lib/schema_migrations.js +67 -13
- package/package.json +20 -13
- package/runtime/paths.js +49 -5
- package/server/guest_agent.js +52 -30
- package/server/http/middleware.js +24 -0
- package/server/http/routes.js +4 -6
- 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 +30803 -30805
- package/server/routes/admin.js +1 -1
- package/server/routes/android.js +30 -34
- package/server/routes/browser.js +23 -15
- package/server/routes/desktop.js +18 -1
- package/server/routes/integrations.js +5 -1
- package/server/routes/memory.js +1 -0
- package/server/routes/settings.js +16 -5
- package/server/routes/social_reach.js +12 -3
- package/server/routes/social_video.js +4 -0
- package/server/services/ai/compaction.js +7 -2
- package/server/services/ai/history.js +44 -5
- package/server/services/ai/integrated_tools/http_request.js +8 -0
- package/server/services/ai/loop/agent_engine_core.js +347 -162
- package/server/services/ai/loop/callbacks.js +1 -0
- package/server/services/ai/loop/completion_judge.js +12 -0
- package/server/services/ai/loop/conversation_loop.js +348 -242
- package/server/services/ai/loop/messaging_delivery.js +129 -57
- package/server/services/ai/loop/model_call_guard.js +91 -0
- package/server/services/ai/loop/model_io.js +8 -41
- package/server/services/ai/loop/tool_dispatch.js +19 -8
- package/server/services/ai/loopPolicy.js +24 -19
- package/server/services/ai/model_discovery.js +227 -0
- package/server/services/ai/model_identity.js +71 -0
- package/server/services/ai/models.js +67 -162
- package/server/services/ai/providerRetry.js +17 -59
- package/server/services/ai/provider_selector.js +111 -0
- package/server/services/ai/providers/anthropic.js +2 -2
- package/server/services/ai/providers/claudeCode.js +15 -28
- package/server/services/ai/providers/githubCopilot.js +36 -16
- package/server/services/ai/providers/google.js +23 -5
- package/server/services/ai/providers/grok.js +4 -3
- package/server/services/ai/providers/grokOauth.js +13 -22
- package/server/services/ai/providers/nvidia.js +10 -5
- package/server/services/ai/providers/ollama.js +102 -82
- package/server/services/ai/providers/ollama_stream.js +142 -0
- package/server/services/ai/providers/openai.js +39 -5
- package/server/services/ai/providers/openaiCodex.js +11 -4
- package/server/services/ai/providers/openrouter.js +29 -7
- package/server/services/ai/providers/provider_error.js +36 -0
- package/server/services/ai/settings.js +26 -2
- package/server/services/ai/taskAnalysis.js +5 -1
- package/server/services/ai/terminal_reply.js +45 -0
- package/server/services/ai/toolEvidence.js +58 -29
- package/server/services/ai/tools.js +124 -111
- package/server/services/android/controller.js +770 -237
- package/server/services/android/process.js +140 -0
- package/server/services/android/sdk_download.js +143 -0
- package/server/services/android/uia.js +6 -5
- package/server/services/artifacts/store.js +24 -0
- package/server/services/browser/controller.js +736 -385
- package/server/services/browser/extension/gateway.js +40 -16
- package/server/services/browser/extension/protocol.js +12 -1
- package/server/services/browser/extension/provider.js +59 -47
- package/server/services/browser/extension/registry.js +155 -34
- package/server/services/cli/executor.js +62 -9
- package/server/services/desktop/gateway.js +41 -4
- package/server/services/desktop/protocol.js +3 -0
- package/server/services/desktop/provider.js +39 -42
- package/server/services/desktop/registry.js +137 -52
- package/server/services/integrations/figma/provider.js +78 -12
- package/server/services/integrations/github/common.js +11 -6
- package/server/services/integrations/github/provider.js +52 -53
- package/server/services/integrations/google/provider.js +55 -19
- package/server/services/integrations/home_assistant/network.js +17 -20
- package/server/services/integrations/home_assistant/provider.js +7 -5
- package/server/services/integrations/home_assistant/tools.js +17 -5
- package/server/services/integrations/http.js +51 -0
- package/server/services/integrations/manager.js +158 -53
- package/server/services/integrations/microsoft/provider.js +80 -13
- package/server/services/integrations/neoarchive/provider.js +55 -29
- package/server/services/integrations/neorecall/client.js +17 -10
- package/server/services/integrations/neorecall/provider.js +20 -11
- package/server/services/integrations/notion/provider.js +16 -13
- package/server/services/integrations/oauth_provider.js +115 -51
- package/server/services/integrations/slack/provider.js +98 -9
- package/server/services/integrations/spotify/provider.js +67 -71
- package/server/services/integrations/trello/provider.js +21 -7
- package/server/services/integrations/weather/provider.js +18 -12
- package/server/services/integrations/whatsapp/provider.js +76 -16
- package/server/services/manager.js +87 -1
- package/server/services/memory/embedding_index.js +20 -8
- package/server/services/memory/embeddings.js +151 -90
- package/server/services/memory/ingestion.js +50 -9
- package/server/services/memory/ingestion_documents.js +13 -3
- package/server/services/memory/manager.js +52 -19
- package/server/services/messaging/automation.js +84 -9
- package/server/services/messaging/http_platforms.js +33 -13
- package/server/services/messaging/inbound_queue.js +78 -24
- package/server/services/messaging/inbound_store.js +224 -0
- package/server/services/messaging/manager.js +326 -51
- package/server/services/messaging/typing_keepalive.js +5 -2
- package/server/services/network/http.js +210 -0
- package/server/services/network/safe_request.js +307 -0
- package/server/services/runtime/backends/local-vm.js +214 -66
- package/server/services/runtime/manager.js +17 -12
- package/server/services/social_reach/channels/github.js +10 -4
- package/server/services/social_reach/channels/reddit.js +4 -4
- package/server/services/social_reach/channels/rss.js +2 -2
- package/server/services/social_reach/channels/social_video.js +12 -7
- package/server/services/social_reach/channels/v2ex.js +21 -8
- package/server/services/social_reach/channels/x.js +2 -2
- package/server/services/social_reach/channels/xueqiu.js +5 -5
- package/server/services/social_reach/service.js +9 -6
- package/server/services/social_reach/utils.js +65 -14
- package/server/services/social_video/service.js +160 -50
- package/server/services/tasks/integration_runtime.js +18 -8
- package/server/services/tasks/runtime.js +39 -4
- package/server/services/voice/agentBridge.js +17 -4
- package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
- package/server/services/voice/liveSession.js +31 -0
- package/server/services/voice/openaiSpeech.js +33 -8
- package/server/services/voice/providers.js +233 -151
- package/server/services/voice/runtimeManager.js +118 -20
- package/server/services/voice/turnRunner.js +6 -0
- package/server/services/wearable/firmware_manifest.js +51 -13
- package/server/services/wearable/service.js +1 -0
- package/server/utils/abort.js +96 -0
- package/server/utils/cloud-security.js +110 -3
- package/server/utils/files.js +31 -0
- package/server/utils/image_payload.js +95 -0
- package/server/utils/retry.js +107 -0
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { createAbortError } = require('../../utils/abort');
|
|
4
|
+
const {
|
|
5
|
+
RETRYABLE_HTTP_STATUS: RETRYABLE_STATUS,
|
|
6
|
+
RETRYABLE_NETWORK_CODES: RETRYABLE_CODES,
|
|
7
|
+
abortableDelay,
|
|
8
|
+
computeBackoffMs,
|
|
9
|
+
getErrorCode,
|
|
10
|
+
getHttpStatus,
|
|
11
|
+
retryAfterMilliseconds,
|
|
12
|
+
} = require('../../utils/retry');
|
|
13
|
+
|
|
3
14
|
// Centralized transient-error retry for AI provider calls.
|
|
4
15
|
//
|
|
5
16
|
// A transient blip (rate limit, provider overload, brief network failure) should
|
|
@@ -14,17 +25,6 @@ const DEFAULTS = {
|
|
|
14
25
|
maxDelayMs: 8000,
|
|
15
26
|
};
|
|
16
27
|
|
|
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
28
|
function readNumberEnv(name, fallback, { min = 0, max = Number.MAX_SAFE_INTEGER } = {}) {
|
|
29
29
|
const raw = process.env[name];
|
|
30
30
|
if (raw === undefined || raw === null || String(raw).trim() === '') return fallback;
|
|
@@ -47,25 +47,10 @@ function resolveConfig(overrides = {}) {
|
|
|
47
47
|
// SDKs disagree on where they put the HTTP status: OpenAI/Anthropic expose
|
|
48
48
|
// `.status`, raw http clients use `.statusCode`, and some nest it under
|
|
49
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
50
|
function isTransientError(err) {
|
|
66
51
|
if (!err) return false;
|
|
67
52
|
|
|
68
|
-
const status =
|
|
53
|
+
const status = getHttpStatus(err);
|
|
69
54
|
if (status !== null) return RETRYABLE_STATUS.has(status);
|
|
70
55
|
|
|
71
56
|
const code = getErrorCode(err);
|
|
@@ -84,39 +69,11 @@ function isTransientError(err) {
|
|
|
84
69
|
// own backoff. Supports both delta-seconds and `retry-after-ms` style headers.
|
|
85
70
|
function retryAfterMs(err) {
|
|
86
71
|
if (!err || typeof err !== 'object') return null;
|
|
87
|
-
|
|
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));
|
|
72
|
+
return retryAfterMilliseconds(err.headers || err.response?.headers);
|
|
116
73
|
}
|
|
117
74
|
|
|
118
|
-
function
|
|
119
|
-
return
|
|
75
|
+
function abortError(signal) {
|
|
76
|
+
return createAbortError(signal, 'Provider retry aborted.');
|
|
120
77
|
}
|
|
121
78
|
|
|
122
79
|
/**
|
|
@@ -137,6 +94,7 @@ async function withProviderRetry(fn, options = {}) {
|
|
|
137
94
|
|
|
138
95
|
let attempt = 0;
|
|
139
96
|
while (true) {
|
|
97
|
+
if (options.signal?.aborted) throw abortError(options.signal);
|
|
140
98
|
attempt += 1;
|
|
141
99
|
try {
|
|
142
100
|
return await fn(attempt);
|
|
@@ -153,7 +111,7 @@ async function withProviderRetry(fn, options = {}) {
|
|
|
153
111
|
options.onRetry({ attempt, delayMs: waitMs, error: err });
|
|
154
112
|
} catch { /* a misbehaving progress callback must not abort the retry */ }
|
|
155
113
|
}
|
|
156
|
-
await
|
|
114
|
+
await abortableDelay(waitMs, options.signal);
|
|
157
115
|
}
|
|
158
116
|
}
|
|
159
117
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getAiSettings } = require('./settings');
|
|
4
|
+
const { createProviderInstance, getSupportedModels } = require('./models');
|
|
5
|
+
const {
|
|
6
|
+
getRawModelId,
|
|
7
|
+
normalizeModelSelections,
|
|
8
|
+
resolveModelSelection,
|
|
9
|
+
} = require('./model_identity');
|
|
10
|
+
|
|
11
|
+
function buildSelection(model, userId, providerConfig) {
|
|
12
|
+
return {
|
|
13
|
+
provider: createProviderInstance(model.provider, userId, providerConfig),
|
|
14
|
+
model: getRawModelId(model),
|
|
15
|
+
modelSelectionId: model.id,
|
|
16
|
+
providerName: model.provider,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function getProviderForUser(
|
|
21
|
+
userId,
|
|
22
|
+
_task = '',
|
|
23
|
+
isSubagent = false,
|
|
24
|
+
modelOverride = null,
|
|
25
|
+
providerConfig = {},
|
|
26
|
+
) {
|
|
27
|
+
const agentId = providerConfig.agentId || null;
|
|
28
|
+
const aiSettings = getAiSettings(userId, agentId);
|
|
29
|
+
const models = await getSupportedModels(userId, agentId, {
|
|
30
|
+
signal: providerConfig.signal,
|
|
31
|
+
});
|
|
32
|
+
const selectableModels = models.filter((model) => model.available !== false);
|
|
33
|
+
|
|
34
|
+
if (modelOverride && typeof modelOverride === 'string') {
|
|
35
|
+
const requested = resolveModelSelection(selectableModels, modelOverride);
|
|
36
|
+
if (!requested) {
|
|
37
|
+
throw new Error(`Requested model '${modelOverride.trim()}' is not available.`);
|
|
38
|
+
}
|
|
39
|
+
return buildSelection(requested, userId, providerConfig);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const configuredEnabledIds = Array.isArray(aiSettings.enabled_models)
|
|
43
|
+
? aiSettings.enabled_models.map((id) => String(id).trim()).filter(Boolean)
|
|
44
|
+
: [];
|
|
45
|
+
const enabledIds = normalizeModelSelections(selectableModels, configuredEnabledIds);
|
|
46
|
+
const availableModels = configuredEnabledIds.length > 0
|
|
47
|
+
? selectableModels.filter((model) => enabledIds.includes(model.id))
|
|
48
|
+
: selectableModels;
|
|
49
|
+
|
|
50
|
+
if (availableModels.length === 0) {
|
|
51
|
+
const message = configuredEnabledIds.length > 0
|
|
52
|
+
? 'None of the enabled AI models are currently available. Check model and provider settings.'
|
|
53
|
+
: 'No AI providers are currently available. Open Settings and configure at least one provider.';
|
|
54
|
+
throw new Error(message);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const fallbackModel = availableModels[0];
|
|
58
|
+
const userSelectedDefault = isSubagent
|
|
59
|
+
? (aiSettings.default_subagent_model || 'auto')
|
|
60
|
+
: (aiSettings.default_chat_model || 'auto');
|
|
61
|
+
const smarterSelection = aiSettings.smarter_model_selector !== false
|
|
62
|
+
&& aiSettings.smarter_model_selector !== 'false';
|
|
63
|
+
|
|
64
|
+
let selectedModel = fallbackModel;
|
|
65
|
+
if (userSelectedDefault !== 'auto') {
|
|
66
|
+
selectedModel = resolveModelSelection(availableModels, userSelectedDefault) || fallbackModel;
|
|
67
|
+
} else {
|
|
68
|
+
const selectionHint = providerConfig.selectionHint && typeof providerConfig.selectionHint === 'object'
|
|
69
|
+
? providerConfig.selectionHint
|
|
70
|
+
: {};
|
|
71
|
+
const preferredPurpose = String(selectionHint.purpose || '').trim().toLowerCase();
|
|
72
|
+
const highAutonomy = selectionHint.autonomyLevel === 'high'
|
|
73
|
+
|| selectionHint.complexity === 'complex';
|
|
74
|
+
const requiredConfidence = String(selectionHint.requiredConfidence || '').trim().toLowerCase();
|
|
75
|
+
const costMode = String(selectionHint.costMode || aiSettings.cost_mode || 'balanced_auto')
|
|
76
|
+
.trim()
|
|
77
|
+
.toLowerCase();
|
|
78
|
+
const requestedPurpose = ['planning', 'coding', 'general', 'fast'].includes(preferredPurpose)
|
|
79
|
+
? preferredPurpose
|
|
80
|
+
: '';
|
|
81
|
+
const priceRank = { free: 0, cheap: 1, medium: 2, expensive: 3 };
|
|
82
|
+
const chooseForPurpose = (purpose) => {
|
|
83
|
+
const candidates = availableModels.filter((model) => model.purpose === purpose);
|
|
84
|
+
if (candidates.length === 0) return null;
|
|
85
|
+
if (['economy', 'cost_saver', 'lowest_cost'].includes(costMode)) {
|
|
86
|
+
return [...candidates].sort((left, right) => (
|
|
87
|
+
(priceRank[left.priceTier] ?? 99) - (priceRank[right.priceTier] ?? 99)
|
|
88
|
+
))[0];
|
|
89
|
+
}
|
|
90
|
+
if (['quality', 'highest_quality'].includes(costMode) || requiredConfidence === 'high') {
|
|
91
|
+
return candidates.find((model) => model.priceTier !== 'free' && model.priceTier !== 'cheap')
|
|
92
|
+
|| candidates[0];
|
|
93
|
+
}
|
|
94
|
+
return candidates[0];
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (smarterSelection && requestedPurpose) {
|
|
98
|
+
selectedModel = chooseForPurpose(requestedPurpose) || fallbackModel;
|
|
99
|
+
} else if (smarterSelection && highAutonomy) {
|
|
100
|
+
selectedModel = chooseForPurpose('planning') || chooseForPurpose('general') || fallbackModel;
|
|
101
|
+
} else if (isSubagent) {
|
|
102
|
+
selectedModel = chooseForPurpose('fast') || fallbackModel;
|
|
103
|
+
} else {
|
|
104
|
+
selectedModel = chooseForPurpose('general') || fallbackModel;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return buildSelection(selectedModel, userId, providerConfig);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { getProviderForUser };
|
|
@@ -31,8 +31,8 @@ class AnthropicProvider extends BaseProvider {
|
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
async listModels() {
|
|
35
|
-
const res = await this.client.models.list({ limit: 100 });
|
|
34
|
+
async listModels(signal = null) {
|
|
35
|
+
const res = await this.client.models.list({ limit: 100 }, { signal });
|
|
36
36
|
return (res.data || []).map((m) => ({ id: m.id, name: m.display_name || m.id }));
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -2,6 +2,8 @@ const os = require('os');
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const Anthropic = require('@anthropic-ai/sdk');
|
|
5
|
+
const { ENV_FILE, upsertEnvValue } = require('../../../../runtime/paths');
|
|
6
|
+
const { fetchResponseText } = require('../../network/http');
|
|
5
7
|
const { AnthropicProvider } = require('./anthropic');
|
|
6
8
|
|
|
7
9
|
const CLAUDE_CLI_CREDS_PATH = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
@@ -12,6 +14,8 @@ const CLAUDE_CODE_SYSTEM_PROMPT = "You are Claude Code, Anthropic's official CLI
|
|
|
12
14
|
const CLAUDE_CODE_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
|
|
13
15
|
const CLAUDE_CODE_TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
|
|
14
16
|
const CLAUDE_CODE_SCOPES = 'user:inference user:profile org:create_api_key user:sessions:claude_code user:mcp_servers';
|
|
17
|
+
const OAUTH_REFRESH_TIMEOUT_MS = 30000;
|
|
18
|
+
const OAUTH_MAX_RESPONSE_BYTES = 256 * 1024;
|
|
15
19
|
|
|
16
20
|
function readTokenRecord(data) {
|
|
17
21
|
const tokens = data?.claudeAiOauthTokens || data?.claudeAiOauth || {};
|
|
@@ -71,40 +75,17 @@ function normalizeExpiresAt(data) {
|
|
|
71
75
|
return null;
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
function sanitizeEnvKey(key) {
|
|
75
|
-
return String(key).replace(/[\r\n]/g, '');
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function sanitizeEnvValue(value) {
|
|
79
|
-
return String(value).replace(/[\r\n]/g, '');
|
|
80
|
-
}
|
|
81
|
-
|
|
82
78
|
function persistEnvValue(key, value) {
|
|
83
79
|
if (!value) return;
|
|
84
80
|
try {
|
|
85
|
-
|
|
86
|
-
const safeKey = sanitizeEnvKey(key);
|
|
87
|
-
const safeValue = sanitizeEnvValue(value);
|
|
88
|
-
const raw = fs.existsSync(ENV_FILE) ? fs.readFileSync(ENV_FILE, 'utf8') : '';
|
|
89
|
-
const lines = raw ? raw.split('\n') : [];
|
|
90
|
-
let replaced = false;
|
|
91
|
-
for (let i = 0; i < lines.length; i++) {
|
|
92
|
-
if (lines[i].startsWith(`${safeKey}=`)) {
|
|
93
|
-
lines[i] = `${safeKey}=${safeValue}`;
|
|
94
|
-
replaced = true;
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
if (!replaced) lines.push(`${safeKey}=${safeValue}`);
|
|
99
|
-
const output = lines.filter((_, idx, arr) => idx !== arr.length - 1 || arr[idx] !== '').join('\n') + '\n';
|
|
100
|
-
fs.mkdirSync(path.dirname(ENV_FILE), { recursive: true });
|
|
101
|
-
fs.writeFileSync(ENV_FILE, output, { mode: 0o600 });
|
|
81
|
+
upsertEnvValue(ENV_FILE, key, value);
|
|
102
82
|
} catch { }
|
|
103
83
|
}
|
|
104
84
|
|
|
105
85
|
async function refreshClaudeCodeAccessToken(refreshToken, fetchImpl = fetch, signal = null) {
|
|
106
86
|
if (!refreshToken) return null;
|
|
107
|
-
const response = await
|
|
87
|
+
const { response, text } = await fetchResponseText(CLAUDE_CODE_TOKEN_URL, {
|
|
88
|
+
fetchImpl,
|
|
108
89
|
method: 'POST',
|
|
109
90
|
headers: {
|
|
110
91
|
'Content-Type': 'application/json',
|
|
@@ -117,9 +98,13 @@ async function refreshClaudeCodeAccessToken(refreshToken, fetchImpl = fetch, sig
|
|
|
117
98
|
client_id: CLAUDE_CODE_CLIENT_ID,
|
|
118
99
|
}),
|
|
119
100
|
signal,
|
|
101
|
+
timeoutMs: OAUTH_REFRESH_TIMEOUT_MS,
|
|
102
|
+
maxResponseBytes: OAUTH_MAX_RESPONSE_BYTES,
|
|
103
|
+
serviceName: 'Claude Code OAuth refresh',
|
|
104
|
+
timeoutCode: 'PROVIDER_OAUTH_TIMEOUT',
|
|
105
|
+
tooLargeCode: 'PROVIDER_OAUTH_RESPONSE_TOO_LARGE',
|
|
120
106
|
});
|
|
121
107
|
|
|
122
|
-
const text = await response.text();
|
|
123
108
|
let data = {};
|
|
124
109
|
try {
|
|
125
110
|
data = text ? JSON.parse(text) : {};
|
|
@@ -128,7 +113,9 @@ async function refreshClaudeCodeAccessToken(refreshToken, fetchImpl = fetch, sig
|
|
|
128
113
|
}
|
|
129
114
|
|
|
130
115
|
if (!response.ok) {
|
|
131
|
-
const detail =
|
|
116
|
+
const detail = String(
|
|
117
|
+
data?.error?.message || data?.error_description || data?.error || text || 'Unknown error',
|
|
118
|
+
).slice(0, 2000);
|
|
132
119
|
throw new Error(`Claude Code OAuth refresh failed: HTTP ${response.status} ${detail}`);
|
|
133
120
|
}
|
|
134
121
|
if (!data.access_token) {
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
const { OpenAIProvider } = require('./openai');
|
|
2
|
+
const {
|
|
3
|
+
fetchResponseText,
|
|
4
|
+
waitForAbortableResult,
|
|
5
|
+
} = require('../../network/http');
|
|
6
|
+
const { sanitizeProviderErrorDetail } = require('./provider_error');
|
|
2
7
|
|
|
3
8
|
class GithubCopilotProvider extends OpenAIProvider {
|
|
4
9
|
constructor(config = {}) {
|
|
@@ -27,35 +32,46 @@ class GithubCopilotProvider extends OpenAIProvider {
|
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
async _refreshCopilotToken(signal = null) {
|
|
30
|
-
if (this._refreshPromise) return this._refreshPromise;
|
|
31
|
-
|
|
32
35
|
const now = Math.floor(Date.now() / 1000);
|
|
33
36
|
// Refresh token if missing or expiring in less than 5 minutes
|
|
34
37
|
if (this.copilotToken && this.tokenExpiresAt >= now + 300) {
|
|
35
38
|
return;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
this._refreshPromise
|
|
39
|
-
|
|
41
|
+
if (!this._refreshPromise) {
|
|
42
|
+
this._refreshPromise = (async () => {
|
|
40
43
|
if (!this.githubToken) {
|
|
41
44
|
throw new Error('GitHub Copilot access token is missing. Please run `neoagent login github-copilot`.');
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
const
|
|
47
|
+
const { response, text } = await fetchResponseText(
|
|
48
|
+
'https://api.github.com/copilot_internal/v2/token',
|
|
49
|
+
{
|
|
45
50
|
headers: {
|
|
46
51
|
'Authorization': `token ${this.githubToken}`,
|
|
47
52
|
'Accept': 'application/json',
|
|
48
53
|
'User-Agent': 'NeoAgent/1.0.0'
|
|
49
54
|
},
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
maxResponseBytes: 1024 * 1024,
|
|
56
|
+
serviceName: 'GitHub Copilot token refresh',
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
const error = new Error(
|
|
62
|
+
`Failed to refresh GitHub Copilot token: HTTP ${response.status} - ${sanitizeProviderErrorDetail(text.slice(0, 500))}`,
|
|
63
|
+
);
|
|
64
|
+
error.status = response.status;
|
|
65
|
+
error.headers = response.headers;
|
|
66
|
+
throw error;
|
|
56
67
|
}
|
|
57
68
|
|
|
58
|
-
|
|
69
|
+
let data;
|
|
70
|
+
try {
|
|
71
|
+
data = JSON.parse(text || '{}');
|
|
72
|
+
} catch {
|
|
73
|
+
throw new Error('Invalid token response from GitHub Copilot.');
|
|
74
|
+
}
|
|
59
75
|
if (!data || typeof data.token !== 'string' || !data.token) {
|
|
60
76
|
throw new Error('Invalid token response from GitHub Copilot.');
|
|
61
77
|
}
|
|
@@ -71,12 +87,16 @@ class GithubCopilotProvider extends OpenAIProvider {
|
|
|
71
87
|
|
|
72
88
|
// Update the client's API key
|
|
73
89
|
this.client.apiKey = this.copilotToken;
|
|
74
|
-
}
|
|
90
|
+
})().finally(() => {
|
|
75
91
|
this._refreshPromise = null;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
78
94
|
|
|
79
|
-
return
|
|
95
|
+
return waitForAbortableResult(
|
|
96
|
+
this._refreshPromise,
|
|
97
|
+
signal,
|
|
98
|
+
'GitHub Copilot token refresh aborted.',
|
|
99
|
+
);
|
|
80
100
|
}
|
|
81
101
|
|
|
82
102
|
async chat(messages, tools = [], options = {}) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { GoogleGenerativeAI } = require('@google/generative-ai');
|
|
2
2
|
const { BaseProvider } = require('./base');
|
|
3
|
+
const { fetchResponseText } = require('../../network/http');
|
|
3
4
|
|
|
4
5
|
class GoogleProvider extends BaseProvider {
|
|
5
6
|
constructor(config = {}) {
|
|
@@ -31,13 +32,30 @@ class GoogleProvider extends BaseProvider {
|
|
|
31
32
|
this.genAI = new GoogleGenerativeAI(this.apiKey);
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
async listModels() {
|
|
35
|
+
async listModels(signal = null) {
|
|
35
36
|
const DROP = /tts|lyria|robotics|deep-research|antigravity|computer-use|-image(?!.*it)/i;
|
|
36
|
-
const
|
|
37
|
-
|
|
37
|
+
const { response, text } = await fetchResponseText(
|
|
38
|
+
'https://generativelanguage.googleapis.com/v1beta/models?pageSize=200',
|
|
39
|
+
{
|
|
40
|
+
headers: { 'x-goog-api-key': this.apiKey },
|
|
41
|
+
maxResponseBytes: 5 * 1024 * 1024,
|
|
42
|
+
serviceName: 'Google model catalog',
|
|
43
|
+
signal,
|
|
44
|
+
},
|
|
38
45
|
);
|
|
39
|
-
if (!
|
|
40
|
-
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const error = new Error(`Google models API returned ${response.status}`);
|
|
48
|
+
error.status = response.status;
|
|
49
|
+
error.headers = response.headers;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
let payload;
|
|
53
|
+
try {
|
|
54
|
+
payload = JSON.parse(text || '{}');
|
|
55
|
+
} catch {
|
|
56
|
+
throw new Error('Google models API returned invalid JSON.');
|
|
57
|
+
}
|
|
58
|
+
const { models = [] } = payload;
|
|
41
59
|
return models
|
|
42
60
|
.filter((m) => {
|
|
43
61
|
const id = m.name.replace('models/', '');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const OpenAI = require('openai');
|
|
2
2
|
const { OpenAICompatibleProvider } = require('./openaiCompatible');
|
|
3
|
+
const { wrapProviderError } = require('./provider_error');
|
|
3
4
|
|
|
4
5
|
class GrokProvider extends OpenAICompatibleProvider {
|
|
5
6
|
constructor(config = {}) {
|
|
@@ -11,15 +12,15 @@ class GrokProvider extends OpenAICompatibleProvider {
|
|
|
11
12
|
});
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
async listModels() {
|
|
15
|
+
async listModels(signal = null) {
|
|
15
16
|
try {
|
|
16
|
-
const res = await this.client.models.list();
|
|
17
|
+
const res = await this.client.models.list({ signal });
|
|
17
18
|
const DROP = /imagine|diffus|embed|-tts/i;
|
|
18
19
|
return res.data
|
|
19
20
|
.filter((m) => !DROP.test(m.id))
|
|
20
21
|
.map((m) => ({ id: m.id, name: m.id }));
|
|
21
22
|
} catch (err) {
|
|
22
|
-
throw
|
|
23
|
+
throw wrapProviderError(err, 'Failed to list Grok models', { signal });
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
1
|
const OpenAI = require('openai');
|
|
2
|
+
const { ENV_FILE, upsertEnvValue } = require('../../../../runtime/paths');
|
|
3
|
+
const { fetchResponseText } = require('../../network/http');
|
|
4
4
|
const { GrokProvider } = require('./grok');
|
|
5
5
|
|
|
6
6
|
const GROK_OAUTH_BASE_URL = 'https://api.x.ai/v1';
|
|
7
7
|
const GROK_OAUTH_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828';
|
|
8
8
|
const GROK_OAUTH_TOKEN_URL = 'https://auth.x.ai/oauth2/token';
|
|
9
9
|
const GROK_OAUTH_SCOPES = 'openid profile email offline_access grok-cli:access api:access';
|
|
10
|
+
const OAUTH_REFRESH_TIMEOUT_MS = 30000;
|
|
11
|
+
const OAUTH_MAX_RESPONSE_BYTES = 256 * 1024;
|
|
10
12
|
|
|
11
13
|
function normalizeExpiresAt(data) {
|
|
12
14
|
if (typeof data.expires_at === 'number' && Number.isFinite(data.expires_at)) {
|
|
@@ -21,29 +23,14 @@ function normalizeExpiresAt(data) {
|
|
|
21
23
|
function persistEnvValue(key, value) {
|
|
22
24
|
if (!value) return;
|
|
23
25
|
try {
|
|
24
|
-
|
|
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 });
|
|
26
|
+
upsertEnvValue(ENV_FILE, key, value);
|
|
41
27
|
} catch { }
|
|
42
28
|
}
|
|
43
29
|
|
|
44
30
|
async function refreshGrokOAuthAccessToken(refreshToken, fetchImpl = fetch, signal = null) {
|
|
45
31
|
if (!refreshToken) return null;
|
|
46
|
-
const response = await
|
|
32
|
+
const { response, text } = await fetchResponseText(GROK_OAUTH_TOKEN_URL, {
|
|
33
|
+
fetchImpl,
|
|
47
34
|
method: 'POST',
|
|
48
35
|
headers: {
|
|
49
36
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -55,9 +42,13 @@ async function refreshGrokOAuthAccessToken(refreshToken, fetchImpl = fetch, sign
|
|
|
55
42
|
client_id: GROK_OAUTH_CLIENT_ID,
|
|
56
43
|
}),
|
|
57
44
|
signal,
|
|
45
|
+
timeoutMs: OAUTH_REFRESH_TIMEOUT_MS,
|
|
46
|
+
maxResponseBytes: OAUTH_MAX_RESPONSE_BYTES,
|
|
47
|
+
serviceName: 'Grok OAuth refresh',
|
|
48
|
+
timeoutCode: 'PROVIDER_OAUTH_TIMEOUT',
|
|
49
|
+
tooLargeCode: 'PROVIDER_OAUTH_RESPONSE_TOO_LARGE',
|
|
58
50
|
});
|
|
59
51
|
|
|
60
|
-
const text = await response.text();
|
|
61
52
|
let data = {};
|
|
62
53
|
try {
|
|
63
54
|
data = text ? JSON.parse(text) : {};
|
|
@@ -69,7 +60,7 @@ async function refreshGrokOAuthAccessToken(refreshToken, fetchImpl = fetch, sign
|
|
|
69
60
|
if (data?.error === 'invalid_grant') {
|
|
70
61
|
throw new Error('Grok OAuth refresh token is invalid or expired. Re-run `neoagent login grok-oauth` to re-authenticate.');
|
|
71
62
|
}
|
|
72
|
-
const detail = data?.error_description || data?.error || text || 'Unknown error';
|
|
63
|
+
const detail = String(data?.error_description || data?.error || text || 'Unknown error').slice(0, 2000);
|
|
73
64
|
throw new Error(`Grok OAuth refresh failed: HTTP ${response.status} ${detail}`);
|
|
74
65
|
}
|
|
75
66
|
if (!data.access_token) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const OpenAI = require('openai');
|
|
2
2
|
const { OpenAICompatibleProvider } = require('./openaiCompatible');
|
|
3
|
+
const { wrapProviderError } = require('./provider_error');
|
|
3
4
|
|
|
4
5
|
const NVIDIA_BASE_URL = 'https://integrate.api.nvidia.com/v1';
|
|
5
6
|
|
|
@@ -32,15 +33,15 @@ class NvidiaProvider extends OpenAICompatibleProvider {
|
|
|
32
33
|
});
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
async listModels() {
|
|
36
|
+
async listModels(signal = null) {
|
|
36
37
|
try {
|
|
37
|
-
const res = await this.client.models.list();
|
|
38
|
+
const res = await this.client.models.list({ signal });
|
|
38
39
|
const DROP = /embed|bge|e5-|rerank|guard|safety|moderat|diffus|flux|stable|imagen|vision-enc|whisper|tts|speech|paraphrase|classif/i;
|
|
39
40
|
return res.data
|
|
40
41
|
.filter((m) => !DROP.test(m.id))
|
|
41
42
|
.map((m) => ({ id: m.id, name: m.id }));
|
|
42
43
|
} catch (err) {
|
|
43
|
-
throw
|
|
44
|
+
throw wrapProviderError(err, 'NVIDIA NIM request failed', { signal });
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -78,7 +79,9 @@ class NvidiaProvider extends OpenAICompatibleProvider {
|
|
|
78
79
|
try {
|
|
79
80
|
response = await this.client.chat.completions.create(params, { signal: options.signal });
|
|
80
81
|
} catch (err) {
|
|
81
|
-
throw
|
|
82
|
+
throw wrapProviderError(err, 'NVIDIA NIM request failed', {
|
|
83
|
+
signal: options.signal,
|
|
84
|
+
});
|
|
82
85
|
}
|
|
83
86
|
return this.normalizeResponse(response);
|
|
84
87
|
}
|
|
@@ -95,7 +98,9 @@ class NvidiaProvider extends OpenAICompatibleProvider {
|
|
|
95
98
|
try {
|
|
96
99
|
stream = await this.client.chat.completions.create(params, { signal: options.signal });
|
|
97
100
|
} catch (err) {
|
|
98
|
-
throw
|
|
101
|
+
throw wrapProviderError(err, 'NVIDIA NIM request failed', {
|
|
102
|
+
signal: options.signal,
|
|
103
|
+
});
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
let toolCalls = [];
|