neoagent 3.2.1-beta.1 → 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 +358 -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 +20 -45
- 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 +21 -33
- package/server/services/ai/providers/githubCopilot.js +41 -20
- 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 +19 -27
- package/server/services/ai/providers/nvidia.js +10 -5
- package/server/services/ai/providers/ollama.js +111 -84
- 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
|
-
async function refreshClaudeCodeAccessToken(refreshToken, fetchImpl = fetch) {
|
|
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',
|
|
@@ -116,9 +97,14 @@ async function refreshClaudeCodeAccessToken(refreshToken, fetchImpl = fetch) {
|
|
|
116
97
|
refresh_token: refreshToken,
|
|
117
98
|
client_id: CLAUDE_CODE_CLIENT_ID,
|
|
118
99
|
}),
|
|
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',
|
|
119
106
|
});
|
|
120
107
|
|
|
121
|
-
const text = await response.text();
|
|
122
108
|
let data = {};
|
|
123
109
|
try {
|
|
124
110
|
data = text ? JSON.parse(text) : {};
|
|
@@ -127,7 +113,9 @@ async function refreshClaudeCodeAccessToken(refreshToken, fetchImpl = fetch) {
|
|
|
127
113
|
}
|
|
128
114
|
|
|
129
115
|
if (!response.ok) {
|
|
130
|
-
const detail =
|
|
116
|
+
const detail = String(
|
|
117
|
+
data?.error?.message || data?.error_description || data?.error || text || 'Unknown error',
|
|
118
|
+
).slice(0, 2000);
|
|
131
119
|
throw new Error(`Claude Code OAuth refresh failed: HTTP ${response.status} ${detail}`);
|
|
132
120
|
}
|
|
133
121
|
if (!data.access_token) {
|
|
@@ -209,8 +197,8 @@ class ClaudeCodeProvider extends AnthropicProvider {
|
|
|
209
197
|
});
|
|
210
198
|
}
|
|
211
199
|
|
|
212
|
-
async refreshClient() {
|
|
213
|
-
const refreshed = await refreshClaudeCodeAccessToken(this.refreshToken, this.fetchImpl);
|
|
200
|
+
async refreshClient(signal = null) {
|
|
201
|
+
const refreshed = await refreshClaudeCodeAccessToken(this.refreshToken, this.fetchImpl, signal);
|
|
214
202
|
if (!refreshed?.access) return false;
|
|
215
203
|
this.authToken = refreshed.access;
|
|
216
204
|
this.refreshToken = refreshed.refresh || this.refreshToken;
|
|
@@ -244,7 +232,7 @@ class ClaudeCodeProvider extends AnthropicProvider {
|
|
|
244
232
|
if ((!isAuthenticationError(err) && !isInferenceScopeError(err)) || !this.refreshToken) {
|
|
245
233
|
throw formatClaudeCodeCredentialError(err);
|
|
246
234
|
}
|
|
247
|
-
await this.refreshClient();
|
|
235
|
+
await this.refreshClient(options.signal);
|
|
248
236
|
try {
|
|
249
237
|
return await super.chat(messages, tools, options);
|
|
250
238
|
} catch (retryErr) {
|
|
@@ -260,7 +248,7 @@ class ClaudeCodeProvider extends AnthropicProvider {
|
|
|
260
248
|
if ((!isAuthenticationError(err) && !isInferenceScopeError(err)) || !this.refreshToken) {
|
|
261
249
|
throw formatClaudeCodeCredentialError(err);
|
|
262
250
|
}
|
|
263
|
-
await this.refreshClient();
|
|
251
|
+
await this.refreshClient(options.signal);
|
|
264
252
|
try {
|
|
265
253
|
yield* super.stream(messages, tools, options);
|
|
266
254
|
} catch (retryErr) {
|
|
@@ -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 = {}) {
|
|
@@ -26,35 +31,47 @@ class GithubCopilotProvider extends OpenAIProvider {
|
|
|
26
31
|
this._refreshPromise = null;
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
async _refreshCopilotToken() {
|
|
30
|
-
if (this._refreshPromise) return this._refreshPromise;
|
|
31
|
-
|
|
34
|
+
async _refreshCopilotToken(signal = null) {
|
|
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
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
},
|
|
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;
|
|
55
67
|
}
|
|
56
68
|
|
|
57
|
-
|
|
69
|
+
let data;
|
|
70
|
+
try {
|
|
71
|
+
data = JSON.parse(text || '{}');
|
|
72
|
+
} catch {
|
|
73
|
+
throw new Error('Invalid token response from GitHub Copilot.');
|
|
74
|
+
}
|
|
58
75
|
if (!data || typeof data.token !== 'string' || !data.token) {
|
|
59
76
|
throw new Error('Invalid token response from GitHub Copilot.');
|
|
60
77
|
}
|
|
@@ -70,26 +87,30 @@ class GithubCopilotProvider extends OpenAIProvider {
|
|
|
70
87
|
|
|
71
88
|
// Update the client's API key
|
|
72
89
|
this.client.apiKey = this.copilotToken;
|
|
73
|
-
}
|
|
90
|
+
})().finally(() => {
|
|
74
91
|
this._refreshPromise = null;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
77
94
|
|
|
78
|
-
return
|
|
95
|
+
return waitForAbortableResult(
|
|
96
|
+
this._refreshPromise,
|
|
97
|
+
signal,
|
|
98
|
+
'GitHub Copilot token refresh aborted.',
|
|
99
|
+
);
|
|
79
100
|
}
|
|
80
101
|
|
|
81
102
|
async chat(messages, tools = [], options = {}) {
|
|
82
|
-
await this._refreshCopilotToken();
|
|
103
|
+
await this._refreshCopilotToken(options.signal);
|
|
83
104
|
return super.chat(messages, tools, options);
|
|
84
105
|
}
|
|
85
106
|
|
|
86
107
|
async *stream(messages, tools = [], options = {}) {
|
|
87
|
-
await this._refreshCopilotToken();
|
|
108
|
+
await this._refreshCopilotToken(options.signal);
|
|
88
109
|
yield* super.stream(messages, tools, options);
|
|
89
110
|
}
|
|
90
111
|
|
|
91
112
|
async analyzeImage(options = {}) {
|
|
92
|
-
await this._refreshCopilotToken();
|
|
113
|
+
await this._refreshCopilotToken(options.signal);
|
|
93
114
|
return super.analyzeImage(options);
|
|
94
115
|
}
|
|
95
116
|
}
|
|
@@ -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
|
-
async function refreshGrokOAuthAccessToken(refreshToken, fetchImpl = fetch) {
|
|
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',
|
|
@@ -54,9 +41,14 @@ async function refreshGrokOAuthAccessToken(refreshToken, fetchImpl = fetch) {
|
|
|
54
41
|
refresh_token: refreshToken,
|
|
55
42
|
client_id: GROK_OAUTH_CLIENT_ID,
|
|
56
43
|
}),
|
|
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',
|
|
57
50
|
});
|
|
58
51
|
|
|
59
|
-
const text = await response.text();
|
|
60
52
|
let data = {};
|
|
61
53
|
try {
|
|
62
54
|
data = text ? JSON.parse(text) : {};
|
|
@@ -68,7 +60,7 @@ async function refreshGrokOAuthAccessToken(refreshToken, fetchImpl = fetch) {
|
|
|
68
60
|
if (data?.error === 'invalid_grant') {
|
|
69
61
|
throw new Error('Grok OAuth refresh token is invalid or expired. Re-run `neoagent login grok-oauth` to re-authenticate.');
|
|
70
62
|
}
|
|
71
|
-
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);
|
|
72
64
|
throw new Error(`Grok OAuth refresh failed: HTTP ${response.status} ${detail}`);
|
|
73
65
|
}
|
|
74
66
|
if (!data.access_token) {
|
|
@@ -102,8 +94,8 @@ class GrokOAuthProvider extends GrokProvider {
|
|
|
102
94
|
this.fetchImpl = config.fetch || fetch;
|
|
103
95
|
}
|
|
104
96
|
|
|
105
|
-
async refreshClient() {
|
|
106
|
-
const refreshed = await refreshGrokOAuthAccessToken(this.refreshToken, this.fetchImpl);
|
|
97
|
+
async refreshClient(signal = null) {
|
|
98
|
+
const refreshed = await refreshGrokOAuthAccessToken(this.refreshToken, this.fetchImpl, signal);
|
|
107
99
|
if (!refreshed?.access) return false;
|
|
108
100
|
this.authToken = refreshed.access;
|
|
109
101
|
this.refreshToken = refreshed.refresh || this.refreshToken;
|
|
@@ -122,7 +114,7 @@ class GrokOAuthProvider extends GrokProvider {
|
|
|
122
114
|
return await super.chat(messages, tools, options);
|
|
123
115
|
} catch (err) {
|
|
124
116
|
if (err?.status !== 401 || !this.refreshToken) throw err;
|
|
125
|
-
await this.refreshClient();
|
|
117
|
+
await this.refreshClient(options.signal);
|
|
126
118
|
return await super.chat(messages, tools, options);
|
|
127
119
|
}
|
|
128
120
|
}
|
|
@@ -132,7 +124,7 @@ class GrokOAuthProvider extends GrokProvider {
|
|
|
132
124
|
yield* super.stream(messages, tools, options);
|
|
133
125
|
} catch (err) {
|
|
134
126
|
if (err?.status !== 401 || !this.refreshToken) throw err;
|
|
135
|
-
await this.refreshClient();
|
|
127
|
+
await this.refreshClient(options.signal);
|
|
136
128
|
yield* super.stream(messages, tools, options);
|
|
137
129
|
}
|
|
138
130
|
}
|