neoagent 3.2.1-beta.1 → 3.2.1-beta.11
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 +654 -90
- package/flutter_app/lib/main_chat.dart +118 -739
- package/flutter_app/lib/main_controller.dart +157 -24
- package/flutter_app/lib/main_integrations.dart +607 -8
- package/flutter_app/lib/main_models.dart +7 -2
- package/flutter_app/lib/main_operations.dart +334 -370
- package/flutter_app/lib/main_security.dart +266 -112
- package/flutter_app/lib/main_settings.dart +342 -3
- package/flutter_app/lib/main_shared.dart +14 -11
- package/flutter_app/lib/src/backend_client.dart +97 -0
- 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/landing/index.html +3 -1
- package/lib/manager.js +106 -89
- package/lib/schema_migrations.js +115 -13
- package/package.json +30 -15
- package/runtime/paths.js +49 -5
- package/server/db/database.js +2 -2
- package/server/guest-agent.cli.package.json +13 -0
- package/server/guest_agent.js +85 -40
- package/server/http/middleware.js +24 -0
- package/server/http/routes.js +12 -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 +2 -2
- package/server/public/main.dart.js +81930 -80509
- package/server/routes/admin.js +1 -1
- package/server/routes/android.js +30 -34
- package/server/routes/behavior.js +80 -0
- package/server/routes/browser.js +23 -15
- package/server/routes/desktop.js +18 -1
- package/server/routes/integrations.js +107 -1
- package/server/routes/memory.js +1 -0
- package/server/routes/settings.js +20 -5
- package/server/routes/social_reach.js +12 -3
- package/server/routes/social_video.js +4 -0
- package/server/services/agents/manager.js +1 -1
- package/server/services/ai/capabilityHealth.js +62 -96
- package/server/services/ai/compaction.js +7 -2
- package/server/services/ai/history.js +45 -6
- package/server/services/ai/integrated_tools/http_request.js +8 -0
- package/server/services/ai/loop/agent_engine_core.js +452 -176
- package/server/services/ai/loop/blank_recovery.js +5 -4
- package/server/services/ai/loop/callbacks.js +1 -0
- package/server/services/ai/loop/completion_judge.js +291 -8
- package/server/services/ai/loop/conversation_loop.js +515 -342
- package/server/services/ai/loop/messaging_delivery.js +176 -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/progress_classification.js +2 -0
- package/server/services/ai/loop/tool_dispatch.js +19 -8
- package/server/services/ai/loopPolicy.js +48 -21
- package/server/services/ai/messagingFallback.js +17 -17
- package/server/services/ai/model_discovery.js +227 -0
- package/server/services/ai/model_failure_cache.js +108 -0
- package/server/services/ai/model_identity.js +71 -0
- package/server/services/ai/models.js +68 -163
- package/server/services/ai/providerRetry.js +17 -59
- package/server/services/ai/provider_selector.js +166 -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 +135 -97
- 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/systemPrompt.js +32 -123
- package/server/services/ai/taskAnalysis.js +104 -11
- package/server/services/ai/toolEvidence.js +256 -29
- package/server/services/ai/tools.js +248 -117
- 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/behavior/config.js +251 -0
- package/server/services/behavior/defaults.js +68 -0
- package/server/services/behavior/delivery.js +176 -0
- package/server/services/behavior/index.js +43 -0
- package/server/services/behavior/model_client.js +35 -0
- package/server/services/behavior/modules/agent_identity.js +37 -0
- package/server/services/behavior/modules/channel_style.js +28 -0
- package/server/services/behavior/modules/index.js +29 -0
- package/server/services/behavior/modules/norms.js +101 -0
- package/server/services/behavior/modules/persona.js +48 -0
- package/server/services/behavior/modules/persona_prompt.js +33 -0
- package/server/services/behavior/modules/social_memory.js +86 -0
- package/server/services/behavior/modules/social_observability.js +94 -0
- package/server/services/behavior/modules/social_signals.js +41 -0
- package/server/services/behavior/modules/theory_of_mind.js +110 -0
- package/server/services/behavior/modules/turn_taking.js +237 -0
- package/server/services/behavior/pipeline.js +285 -0
- package/server/services/behavior/registry.js +78 -0
- package/server/services/behavior/signals.js +107 -0
- package/server/services/behavior/state.js +99 -0
- package/server/services/behavior/system_prompt.js +75 -0
- package/server/services/browser/controller.js +843 -385
- package/server/services/browser/extension/gateway.js +40 -16
- package/server/services/browser/extension/protocol.js +15 -1
- package/server/services/browser/extension/provider.js +71 -47
- package/server/services/browser/extension/registry.js +155 -34
- package/server/services/cli/executor.js +62 -9
- package/server/services/credentials/bitwarden_cli.js +322 -0
- package/server/services/credentials/broker.js +594 -0
- 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/bitwarden/constants.js +14 -0
- package/server/services/integrations/bitwarden/provider.js +197 -0
- package/server/services/integrations/bitwarden/snapshot.js +65 -0
- 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 +159 -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/registry.js +2 -0
- 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 +110 -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 +66 -52
- package/server/services/messaging/access_policy.js +10 -6
- package/server/services/messaging/automation.js +240 -34
- package/server/services/messaging/discord.js +11 -1
- package/server/services/messaging/formatting_guides.js +5 -4
- package/server/services/messaging/http_platforms.js +37 -16
- package/server/services/messaging/inbound_queue.js +143 -28
- package/server/services/messaging/inbound_store.js +257 -0
- package/server/services/messaging/manager.js +344 -51
- package/server/services/messaging/telegram.js +10 -1
- package/server/services/messaging/typing_keepalive.js +5 -2
- package/server/services/messaging/whatsapp.js +33 -14
- 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 +227 -67
- package/server/services/runtime/docker-vm-manager.js +9 -0
- package/server/services/runtime/guest_bootstrap.js +30 -4
- package/server/services/runtime/guest_image.js +43 -12
- package/server/services/runtime/manager.js +77 -23
- package/server/services/runtime/validation.js +7 -6
- package/server/services/security/tool_categories.js +6 -0
- 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 +13 -8
- 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/captions.js +2 -2
- package/server/services/social_video/service.js +343 -68
- 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/message.js +1 -1
- package/server/services/voice/openaiSpeech.js +33 -8
- package/server/services/voice/providers.js +233 -151
- package/server/services/voice/runtime.js +2 -2
- 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/logger.js +19 -0
- package/server/utils/retry.js +107 -0
|
@@ -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 = [];
|
|
@@ -1,4 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
const { BaseProvider } = require('./base');
|
|
4
|
+
const { fetchResponseText, readResponseText } = require('../../network/http');
|
|
5
|
+
const { createAbortError, isAbortError, throwIfAborted } = require('../../../utils/abort');
|
|
6
|
+
const { readOllamaStream } = require('./ollama_stream');
|
|
7
|
+
|
|
8
|
+
const MAX_CHAT_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
9
|
+
|
|
10
|
+
function ollamaError(message, status = null) {
|
|
11
|
+
const error = new Error(
|
|
12
|
+
status == null
|
|
13
|
+
? `Ollama request failed: ${message}`
|
|
14
|
+
: `Ollama request failed (HTTP ${status}): ${message}`,
|
|
15
|
+
);
|
|
16
|
+
if (status != null) error.status = status;
|
|
17
|
+
if (/does not support tools|tools.*not supported/i.test(String(message || ''))) {
|
|
18
|
+
error.code = 'OLLAMA_TOOLS_UNSUPPORTED';
|
|
19
|
+
}
|
|
20
|
+
return error;
|
|
21
|
+
}
|
|
2
22
|
|
|
3
23
|
class OllamaProvider extends BaseProvider {
|
|
4
24
|
constructor(config = {}) {
|
|
@@ -8,22 +28,26 @@ class OllamaProvider extends BaseProvider {
|
|
|
8
28
|
this.models = [];
|
|
9
29
|
}
|
|
10
30
|
|
|
11
|
-
async listModels() {
|
|
31
|
+
async listModels(signal = null) {
|
|
12
32
|
try {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
33
|
+
const { response, text } = await fetchResponseText(`${this.baseUrl}/api/tags`, {
|
|
34
|
+
maxResponseBytes: 2 * 1024 * 1024,
|
|
35
|
+
serviceName: 'Ollama model catalog',
|
|
36
|
+
signal,
|
|
37
|
+
timeoutMs: 5000,
|
|
38
|
+
});
|
|
39
|
+
if (!response.ok) throw new Error(`Ollama /api/tags returned HTTP ${response.status}`);
|
|
40
|
+
const data = JSON.parse(text || '{}');
|
|
18
41
|
this.models = (data.models || []).map(m => m.name);
|
|
19
42
|
return this.models;
|
|
20
|
-
} catch {
|
|
43
|
+
} catch (err) {
|
|
44
|
+
if (isAbortError(err, signal)) throw createAbortError(signal);
|
|
21
45
|
return [];
|
|
22
46
|
}
|
|
23
47
|
}
|
|
24
48
|
|
|
25
|
-
async ensureModel(model) {
|
|
26
|
-
const models = await this.listModels();
|
|
49
|
+
async ensureModel(model, signal = null) {
|
|
50
|
+
const models = await this.listModels(signal);
|
|
27
51
|
// Normalization: Ollama often adds :latest if no tag is specified
|
|
28
52
|
const normalizedModel = model.includes(':') ? model : `${model}:latest`;
|
|
29
53
|
const found = models.some(m => m === model || m === normalizedModel);
|
|
@@ -39,12 +63,22 @@ class OllamaProvider extends BaseProvider {
|
|
|
39
63
|
message: `Downloading local Ollama model '${model}'. First-time pulls can take a while.`
|
|
40
64
|
});
|
|
41
65
|
try {
|
|
42
|
-
const
|
|
66
|
+
const { response, text } = await fetchResponseText(`${this.baseUrl}/api/pull`, {
|
|
43
67
|
method: 'POST',
|
|
44
68
|
headers: { 'Content-Type': 'application/json' },
|
|
45
|
-
body: JSON.stringify({ name: model, stream: false })
|
|
69
|
+
body: JSON.stringify({ name: model, stream: false }),
|
|
70
|
+
maxResponseBytes: 2 * 1024 * 1024,
|
|
71
|
+
serviceName: 'Ollama model pull',
|
|
72
|
+
signal,
|
|
73
|
+
timeoutMs: 5 * 60 * 1000,
|
|
46
74
|
});
|
|
47
|
-
if (!
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
let detail = text;
|
|
77
|
+
try { detail = JSON.parse(text || '{}').error || text; } catch {}
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Pull failed with HTTP ${response.status}: ${String(detail || response.statusText).slice(0, 500)}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
48
82
|
console.log(`[Ollama] Model '${model}' pulled successfully.`);
|
|
49
83
|
this.onStatus?.({
|
|
50
84
|
kind: 'model_download',
|
|
@@ -54,9 +88,10 @@ class OllamaProvider extends BaseProvider {
|
|
|
54
88
|
message: `Local Ollama model '${model}' is ready.`
|
|
55
89
|
});
|
|
56
90
|
// Refresh local model list
|
|
57
|
-
await this.listModels();
|
|
91
|
+
await this.listModels(signal);
|
|
58
92
|
return true;
|
|
59
93
|
} catch (e) {
|
|
94
|
+
if (isAbortError(e, signal)) throw createAbortError(signal);
|
|
60
95
|
this.onStatus?.({
|
|
61
96
|
kind: 'model_download',
|
|
62
97
|
status: 'failed',
|
|
@@ -130,35 +165,55 @@ class OllamaProvider extends BaseProvider {
|
|
|
130
165
|
signal,
|
|
131
166
|
});
|
|
132
167
|
if (!res.ok) {
|
|
133
|
-
const detail = await res
|
|
168
|
+
const detail = await readResponseText(res, {
|
|
169
|
+
maxResponseBytes: 64 * 1024,
|
|
170
|
+
serviceName: 'Ollama chat error',
|
|
171
|
+
}).catch(() => '');
|
|
134
172
|
let message = detail;
|
|
135
173
|
try { message = JSON.parse(detail)?.error || detail; } catch {}
|
|
136
|
-
|
|
137
|
-
if (/does not support tools|tools.*not supported/i.test(message)) {
|
|
138
|
-
err.code = 'OLLAMA_TOOLS_UNSUPPORTED';
|
|
139
|
-
}
|
|
140
|
-
throw err;
|
|
174
|
+
throw ollamaError(message || res.statusText, res.status);
|
|
141
175
|
}
|
|
142
176
|
return res;
|
|
143
177
|
}
|
|
144
178
|
|
|
179
|
+
async readChatResponse(body, signal = null) {
|
|
180
|
+
const res = await this.postChat(body, signal);
|
|
181
|
+
const text = await readResponseText(res, {
|
|
182
|
+
maxResponseBytes: MAX_CHAT_RESPONSE_BYTES,
|
|
183
|
+
serviceName: 'Ollama chat',
|
|
184
|
+
});
|
|
185
|
+
throwIfAborted(signal, 'Ollama chat aborted.');
|
|
186
|
+
let data;
|
|
187
|
+
try {
|
|
188
|
+
data = JSON.parse(text);
|
|
189
|
+
} catch (cause) {
|
|
190
|
+
throw new Error('Ollama /api/chat returned malformed JSON.', { cause });
|
|
191
|
+
}
|
|
192
|
+
if (data.error) throw ollamaError(data.error);
|
|
193
|
+
return data;
|
|
194
|
+
}
|
|
195
|
+
|
|
145
196
|
async chat(messages, tools = [], options = {}) {
|
|
146
197
|
const model = options.model || this.config.model || 'llama3.1';
|
|
147
|
-
await this.ensureModel(model);
|
|
198
|
+
await this.ensureModel(model, options.signal);
|
|
148
199
|
|
|
149
|
-
let
|
|
200
|
+
let data;
|
|
150
201
|
try {
|
|
151
|
-
|
|
202
|
+
data = await this.readChatResponse(
|
|
203
|
+
this.buildChatBody(messages, tools, { ...options, model }, false),
|
|
204
|
+
options.signal,
|
|
205
|
+
);
|
|
152
206
|
} catch (err) {
|
|
153
207
|
if (err.code === 'OLLAMA_TOOLS_UNSUPPORTED' && tools.length > 0) {
|
|
154
208
|
console.warn(`[Ollama] Model '${model}' does not support tools; retrying without them.`);
|
|
155
|
-
|
|
209
|
+
data = await this.readChatResponse(
|
|
210
|
+
this.buildChatBody(messages, [], { ...options, model }, false),
|
|
211
|
+
options.signal,
|
|
212
|
+
);
|
|
156
213
|
} else {
|
|
157
214
|
throw err;
|
|
158
215
|
}
|
|
159
216
|
}
|
|
160
|
-
|
|
161
|
-
const data = await res.json();
|
|
162
217
|
const msg = data.message || {};
|
|
163
218
|
|
|
164
219
|
return {
|
|
@@ -183,67 +238,39 @@ class OllamaProvider extends BaseProvider {
|
|
|
183
238
|
|
|
184
239
|
async *stream(messages, tools = [], options = {}) {
|
|
185
240
|
const model = options.model || this.config.model || 'llama3.1';
|
|
186
|
-
await this.ensureModel(model);
|
|
187
|
-
|
|
188
|
-
let res;
|
|
189
|
-
try {
|
|
190
|
-
res = await this.postChat(this.buildChatBody(messages, tools, { ...options, model }, true), options.signal);
|
|
191
|
-
} catch (err) {
|
|
192
|
-
if (err.code === 'OLLAMA_TOOLS_UNSUPPORTED' && tools.length > 0) {
|
|
193
|
-
console.warn(`[Ollama] Model '${model}' does not support tools; retrying stream without them.`);
|
|
194
|
-
res = await this.postChat(this.buildChatBody(messages, [], { ...options, model }, true), options.signal);
|
|
195
|
-
} else {
|
|
196
|
-
throw err;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const reader = res.body.getReader();
|
|
201
|
-
const decoder = new TextDecoder();
|
|
202
|
-
let content = '';
|
|
203
|
-
let buffer = '';
|
|
204
|
-
let accumulatedToolCalls = [];
|
|
241
|
+
await this.ensureModel(model, options.signal);
|
|
205
242
|
|
|
243
|
+
let requestTools = tools;
|
|
244
|
+
let retriedWithoutTools = false;
|
|
206
245
|
while (true) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
type: 'done',
|
|
236
|
-
content,
|
|
237
|
-
toolCalls: accumulatedToolCalls,
|
|
238
|
-
finishReason: accumulatedToolCalls.length > 0 ? 'tool_calls' : 'stop',
|
|
239
|
-
usage: data.prompt_eval_count ? {
|
|
240
|
-
promptTokens: data.prompt_eval_count || 0,
|
|
241
|
-
completionTokens: data.eval_count || 0,
|
|
242
|
-
totalTokens: (data.prompt_eval_count || 0) + (data.eval_count || 0)
|
|
243
|
-
} : null
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
} catch {}
|
|
246
|
+
let emittedChunk = false;
|
|
247
|
+
try {
|
|
248
|
+
const res = await this.postChat(
|
|
249
|
+
this.buildChatBody(messages, requestTools, { ...options, model }, true),
|
|
250
|
+
options.signal,
|
|
251
|
+
);
|
|
252
|
+
for await (const chunk of readOllamaStream(res, {
|
|
253
|
+
errorFactory: ollamaError,
|
|
254
|
+
maxResponseBytes: MAX_CHAT_RESPONSE_BYTES,
|
|
255
|
+
signal: options.signal,
|
|
256
|
+
})) {
|
|
257
|
+
emittedChunk = true;
|
|
258
|
+
yield chunk;
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
} catch (err) {
|
|
262
|
+
if (
|
|
263
|
+
err.code === 'OLLAMA_TOOLS_UNSUPPORTED'
|
|
264
|
+
&& tools.length > 0
|
|
265
|
+
&& !retriedWithoutTools
|
|
266
|
+
&& !emittedChunk
|
|
267
|
+
) {
|
|
268
|
+
console.warn(`[Ollama] Model '${model}' does not support tools; retrying stream without them.`);
|
|
269
|
+
retriedWithoutTools = true;
|
|
270
|
+
requestTools = [];
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
throw err;
|
|
247
274
|
}
|
|
248
275
|
}
|
|
249
276
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { waitForAbortableResult } = require('../../network/http');
|
|
4
|
+
const { throwIfAborted } = require('../../../utils/abort');
|
|
5
|
+
|
|
6
|
+
function malformedStreamError(cause = null) {
|
|
7
|
+
const error = new Error('Ollama /api/chat returned malformed streaming JSON.', cause ? { cause } : undefined);
|
|
8
|
+
error.code = 'OLLAMA_STREAM_MALFORMED';
|
|
9
|
+
return error;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function streamEndedEarlyError() {
|
|
13
|
+
const error = new Error('Ollama stream ended before sending a completion marker.');
|
|
14
|
+
error.code = 'OLLAMA_STREAM_INCOMPLETE';
|
|
15
|
+
return error;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseLine(line) {
|
|
19
|
+
if (!line.trim()) return null;
|
|
20
|
+
try {
|
|
21
|
+
const value = JSON.parse(line);
|
|
22
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
23
|
+
throw malformedStreamError();
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error?.code === 'OLLAMA_STREAM_MALFORMED') throw error;
|
|
28
|
+
throw malformedStreamError(error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function usageFrom(data) {
|
|
33
|
+
if (data.prompt_eval_count == null && data.eval_count == null) return null;
|
|
34
|
+
const promptTokens = Number(data.prompt_eval_count) || 0;
|
|
35
|
+
const completionTokens = Number(data.eval_count) || 0;
|
|
36
|
+
return {
|
|
37
|
+
promptTokens,
|
|
38
|
+
completionTokens,
|
|
39
|
+
totalTokens: promptTokens + completionTokens,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function consumeData(data, state, errorFactory) {
|
|
44
|
+
if (data.error) throw errorFactory(data.error);
|
|
45
|
+
const chunks = [];
|
|
46
|
+
if (data.message?.content) {
|
|
47
|
+
const content = String(data.message.content);
|
|
48
|
+
state.content += content;
|
|
49
|
+
chunks.push({ type: 'content', content });
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(data.message?.tool_calls)) {
|
|
52
|
+
for (const toolCall of data.message.tool_calls) {
|
|
53
|
+
const name = String(toolCall?.function?.name || '').trim();
|
|
54
|
+
if (!name) throw malformedStreamError();
|
|
55
|
+
state.toolCalls.push({
|
|
56
|
+
id: `call_ollama_${state.callSeed}_${state.nextCallIndex}`,
|
|
57
|
+
type: 'function',
|
|
58
|
+
function: {
|
|
59
|
+
name,
|
|
60
|
+
arguments: typeof toolCall.function.arguments === 'string'
|
|
61
|
+
? toolCall.function.arguments
|
|
62
|
+
: JSON.stringify(toolCall.function.arguments || {}),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
state.nextCallIndex += 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (data.done === true) {
|
|
69
|
+
state.done = true;
|
|
70
|
+
chunks.push({
|
|
71
|
+
type: 'done',
|
|
72
|
+
content: state.content,
|
|
73
|
+
toolCalls: state.toolCalls,
|
|
74
|
+
finishReason: state.toolCalls.length > 0 ? 'tool_calls' : 'stop',
|
|
75
|
+
usage: usageFrom(data),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return chunks;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function* readOllamaStream(response, options = {}) {
|
|
82
|
+
const reader = response?.body?.getReader?.();
|
|
83
|
+
if (!reader) throw new Error('Ollama /api/chat returned no streaming response body.');
|
|
84
|
+
const decoder = new TextDecoder();
|
|
85
|
+
const maxResponseBytes = Number(options.maxResponseBytes) > 0
|
|
86
|
+
? Number(options.maxResponseBytes)
|
|
87
|
+
: 16 * 1024 * 1024;
|
|
88
|
+
const errorFactory = options.errorFactory || ((message) => new Error(String(message || 'Ollama error')));
|
|
89
|
+
const state = {
|
|
90
|
+
callSeed: Date.now(),
|
|
91
|
+
content: '',
|
|
92
|
+
done: false,
|
|
93
|
+
nextCallIndex: 0,
|
|
94
|
+
toolCalls: [],
|
|
95
|
+
};
|
|
96
|
+
let buffer = '';
|
|
97
|
+
let responseBytes = 0;
|
|
98
|
+
|
|
99
|
+
const emitLine = (line) => {
|
|
100
|
+
const data = parseLine(line);
|
|
101
|
+
return data ? consumeData(data, state, errorFactory) : [];
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
while (!state.done) {
|
|
106
|
+
throwIfAborted(options.signal, 'Ollama stream aborted.');
|
|
107
|
+
const { done, value } = await waitForAbortableResult(
|
|
108
|
+
reader.read(),
|
|
109
|
+
options.signal,
|
|
110
|
+
'Ollama stream aborted.',
|
|
111
|
+
);
|
|
112
|
+
if (done) break;
|
|
113
|
+
responseBytes += value?.byteLength || 0;
|
|
114
|
+
if (responseBytes > maxResponseBytes) {
|
|
115
|
+
const error = new Error('Ollama stream exceeded its response safety limit.');
|
|
116
|
+
error.code = 'HTTP_RESPONSE_TOO_LARGE';
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
buffer += decoder.decode(value, { stream: true });
|
|
121
|
+
const lines = buffer.split('\n');
|
|
122
|
+
buffer = lines.pop() || '';
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
for (const chunk of emitLine(line)) yield chunk;
|
|
125
|
+
if (state.done) return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
buffer += decoder.decode();
|
|
130
|
+
if (buffer.trim()) {
|
|
131
|
+
for (const chunk of emitLine(buffer)) yield chunk;
|
|
132
|
+
}
|
|
133
|
+
if (!state.done) throw streamEndedEarlyError();
|
|
134
|
+
} finally {
|
|
135
|
+
await reader.cancel().catch(() => {});
|
|
136
|
+
reader.releaseLock?.();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
readOllamaStream,
|
|
142
|
+
};
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
const OpenAI = require('openai');
|
|
2
2
|
const { OpenAICompatibleProvider } = require('./openaiCompatible');
|
|
3
|
+
const { wrapProviderError } = require('./provider_error');
|
|
3
4
|
|
|
4
5
|
class OpenAIProvider extends OpenAICompatibleProvider {
|
|
5
6
|
constructor(config = {}) {
|
|
6
7
|
super(config);
|
|
7
8
|
this.name = 'openai';
|
|
8
9
|
this.models = [
|
|
10
|
+
'gpt-5.6',
|
|
11
|
+
'gpt-5.6-sol',
|
|
12
|
+
'gpt-5.6-terra',
|
|
13
|
+
'gpt-5.6-luna',
|
|
9
14
|
'gpt-5.5',
|
|
15
|
+
'gpt-5.4',
|
|
16
|
+
'gpt-5.4-mini',
|
|
17
|
+
'gpt-5.4-nano',
|
|
10
18
|
'gpt-5',
|
|
11
19
|
'gpt-5-mini',
|
|
12
20
|
'gpt-5-nano',
|
|
@@ -16,9 +24,35 @@ class OpenAIProvider extends OpenAICompatibleProvider {
|
|
|
16
24
|
'o4-mini'
|
|
17
25
|
];
|
|
18
26
|
// Reasoning models: no temperature, use max_completion_tokens, support reasoning_effort
|
|
19
|
-
this.reasoningModels = new Set([
|
|
27
|
+
this.reasoningModels = new Set([
|
|
28
|
+
'gpt-5.6',
|
|
29
|
+
'gpt-5.6-sol',
|
|
30
|
+
'gpt-5.6-terra',
|
|
31
|
+
'gpt-5.6-luna',
|
|
32
|
+
'gpt-5.5',
|
|
33
|
+
'gpt-5.4',
|
|
34
|
+
'gpt-5.4-mini',
|
|
35
|
+
'gpt-5.4-nano',
|
|
36
|
+
'gpt-5',
|
|
37
|
+
'gpt-5-mini',
|
|
38
|
+
'gpt-5-nano',
|
|
39
|
+
'gpt-5.2',
|
|
40
|
+
'gpt-5.1',
|
|
41
|
+
'o1',
|
|
42
|
+
'o3',
|
|
43
|
+
'o3-pro',
|
|
44
|
+
'o4-mini',
|
|
45
|
+
'o3-mini',
|
|
46
|
+
]);
|
|
20
47
|
this.contextWindows = {
|
|
21
|
-
'gpt-5.
|
|
48
|
+
'gpt-5.6': 1050000,
|
|
49
|
+
'gpt-5.6-sol': 1050000,
|
|
50
|
+
'gpt-5.6-terra': 1050000,
|
|
51
|
+
'gpt-5.6-luna': 1050000,
|
|
52
|
+
'gpt-5.5': 1050000,
|
|
53
|
+
'gpt-5.4': 1050000,
|
|
54
|
+
'gpt-5.4-mini': 400000,
|
|
55
|
+
'gpt-5.4-nano': 400000,
|
|
22
56
|
'gpt-5': 400000,
|
|
23
57
|
'gpt-5-mini': 400000,
|
|
24
58
|
'gpt-5-nano': 128000,
|
|
@@ -36,15 +70,15 @@ class OpenAIProvider extends OpenAICompatibleProvider {
|
|
|
36
70
|
});
|
|
37
71
|
}
|
|
38
72
|
|
|
39
|
-
async listModels() {
|
|
73
|
+
async listModels(signal = null) {
|
|
40
74
|
try {
|
|
41
|
-
const res = await this.client.models.list();
|
|
75
|
+
const res = await this.client.models.list({ signal });
|
|
42
76
|
const DROP = /dall-e|whisper|tts|embed|moderat|realtime|audio|transcribe|search-api|-image-|babbage|davinci-002|^sora|-instruct/i;
|
|
43
77
|
return res.data
|
|
44
78
|
.filter((m) => !DROP.test(m.id))
|
|
45
79
|
.map((m) => ({ id: m.id, name: m.id }));
|
|
46
80
|
} catch (err) {
|
|
47
|
-
throw
|
|
81
|
+
throw wrapProviderError(err, 'Failed to list OpenAI models', { signal });
|
|
48
82
|
}
|
|
49
83
|
}
|
|
50
84
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
const crypto = require('crypto');
|
|
2
4
|
const OpenAI = require('openai');
|
|
3
5
|
const { BaseProvider } = require('./base');
|
|
6
|
+
const { wrapProviderError } = require('./provider_error');
|
|
4
7
|
|
|
5
8
|
const DEFAULT_BASE_URL = 'https://chatgpt.com/backend-api/codex';
|
|
6
9
|
const OPENAI_CODEX_EMPTY_INPUT_TEXT = ' ';
|
|
@@ -439,8 +442,10 @@ class OpenAICodexProvider extends BaseProvider {
|
|
|
439
442
|
{ headers: this._requestHeaders(), signal: options.signal },
|
|
440
443
|
);
|
|
441
444
|
} catch (err) {
|
|
442
|
-
|
|
443
|
-
|
|
445
|
+
throw wrapProviderError(err, 'OpenAI Codex request failed', {
|
|
446
|
+
detail: formatOpenAIError(err),
|
|
447
|
+
signal: options.signal,
|
|
448
|
+
});
|
|
444
449
|
}
|
|
445
450
|
|
|
446
451
|
const toolCalls = extractToolCalls(response);
|
|
@@ -468,8 +473,10 @@ class OpenAICodexProvider extends BaseProvider {
|
|
|
468
473
|
{ headers: this._requestHeaders(), signal: options.signal },
|
|
469
474
|
);
|
|
470
475
|
} catch (err) {
|
|
471
|
-
|
|
472
|
-
|
|
476
|
+
throw wrapProviderError(err, 'OpenAI Codex request failed', {
|
|
477
|
+
detail: formatOpenAIError(err),
|
|
478
|
+
signal: options.signal,
|
|
479
|
+
});
|
|
473
480
|
}
|
|
474
481
|
|
|
475
482
|
let content = '';
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const OpenAI = require('openai');
|
|
2
2
|
const { OpenAICompatibleProvider } = require('./openaiCompatible');
|
|
3
|
+
const { fetchResponseText } = require('../../network/http');
|
|
4
|
+
const { wrapProviderError } = require('./provider_error');
|
|
3
5
|
|
|
4
6
|
const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
|
|
5
7
|
|
|
@@ -25,12 +27,26 @@ class OpenRouterProvider extends OpenAICompatibleProvider {
|
|
|
25
27
|
});
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
async listModels() {
|
|
29
|
-
const
|
|
30
|
+
async listModels(signal = null) {
|
|
31
|
+
const { response, text } = await fetchResponseText(`${this.baseURL}/models`, {
|
|
30
32
|
headers: { 'Authorization': `Bearer ${this.client.apiKey}` },
|
|
33
|
+
maxResponseBytes: 5 * 1024 * 1024,
|
|
34
|
+
serviceName: 'OpenRouter model catalog',
|
|
35
|
+
signal,
|
|
31
36
|
});
|
|
32
|
-
if (!
|
|
33
|
-
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
const error = new Error(`OpenRouter /models returned HTTP ${response.status}`);
|
|
39
|
+
error.status = response.status;
|
|
40
|
+
error.headers = response.headers;
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
let payload;
|
|
44
|
+
try {
|
|
45
|
+
payload = JSON.parse(text || '{}');
|
|
46
|
+
} catch {
|
|
47
|
+
throw new Error('OpenRouter /models returned invalid JSON.');
|
|
48
|
+
}
|
|
49
|
+
const { data } = payload;
|
|
34
50
|
const models = data || [];
|
|
35
51
|
for (const m of models) {
|
|
36
52
|
if (m.context_length) contextWindowCache.set(m.id, m.context_length);
|
|
@@ -74,7 +90,9 @@ class OpenRouterProvider extends OpenAICompatibleProvider {
|
|
|
74
90
|
try {
|
|
75
91
|
response = await this.client.chat.completions.create(params, { signal: options.signal });
|
|
76
92
|
} catch (err) {
|
|
77
|
-
throw
|
|
93
|
+
throw wrapProviderError(err, 'OpenRouter request failed', {
|
|
94
|
+
signal: options.signal,
|
|
95
|
+
});
|
|
78
96
|
}
|
|
79
97
|
// OpenRouter returns HTTP 200 even for errors (rate limits, model unavailable, etc.)
|
|
80
98
|
const orErr = this._extractOpenRouterError(response);
|
|
@@ -97,7 +115,9 @@ class OpenRouterProvider extends OpenAICompatibleProvider {
|
|
|
97
115
|
try {
|
|
98
116
|
stream = await this.client.chat.completions.create(params, { signal: options.signal });
|
|
99
117
|
} catch (err) {
|
|
100
|
-
throw
|
|
118
|
+
throw wrapProviderError(err, 'OpenRouter request failed', {
|
|
119
|
+
signal: options.signal,
|
|
120
|
+
});
|
|
101
121
|
}
|
|
102
122
|
|
|
103
123
|
let toolCalls = [];
|
|
@@ -148,7 +168,9 @@ class OpenRouterProvider extends OpenAICompatibleProvider {
|
|
|
148
168
|
}
|
|
149
169
|
} catch (err) {
|
|
150
170
|
// Re-throw SDK APIErrors (from OpenRouter SSE error events) with OpenRouter context.
|
|
151
|
-
throw
|
|
171
|
+
throw wrapProviderError(err, 'OpenRouter stream failed', {
|
|
172
|
+
signal: options.signal,
|
|
173
|
+
});
|
|
152
174
|
}
|
|
153
175
|
|
|
154
176
|
if (toolCalls.length > 0) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createAbortError, isAbortError } = require('../../../utils/abort');
|
|
4
|
+
|
|
5
|
+
function sanitizeProviderErrorDetail(value) {
|
|
6
|
+
return String(value || 'Unknown provider error')
|
|
7
|
+
.slice(0, 2000)
|
|
8
|
+
.replace(/\b(Bearer|Basic|token)\s+[A-Za-z0-9._~+/=-]+/gi, '$1 [redacted]')
|
|
9
|
+
.replace(/([?&](?:key|api_key|access_token)=)[^&\s]+/gi, '$1[redacted]')
|
|
10
|
+
.replace(
|
|
11
|
+
/\b(api[_-]?key|access_token|refresh_token|authorization)\b\s*[:=]\s*["']?[^\s,"'}\]]+/gi,
|
|
12
|
+
'$1=[redacted]',
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function wrapProviderError(error, prefix, options = {}) {
|
|
17
|
+
if (options.signal?.aborted) return createAbortError(options.signal);
|
|
18
|
+
if (isAbortError(error)) return error;
|
|
19
|
+
|
|
20
|
+
const detail = typeof options.detail === 'string'
|
|
21
|
+
? options.detail
|
|
22
|
+
: error?.message || String(error);
|
|
23
|
+
const wrapped = new Error(
|
|
24
|
+
`${prefix}: ${sanitizeProviderErrorDetail(detail)}`,
|
|
25
|
+
{ cause: error },
|
|
26
|
+
);
|
|
27
|
+
for (const property of ['status', 'statusCode', 'code', 'headers', 'response', 'type']) {
|
|
28
|
+
if (error?.[property] !== undefined) wrapped[property] = error[property];
|
|
29
|
+
}
|
|
30
|
+
return wrapped;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = {
|
|
34
|
+
sanitizeProviderErrorDetail,
|
|
35
|
+
wrapProviderError,
|
|
36
|
+
};
|