neoagent 2.4.1-beta.9 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +33 -3
- package/LICENSE +111 -56
- package/README.md +8 -3
- package/docs/configuration.md +8 -0
- package/docs/getting-started.md +9 -3
- package/docs/index.md +4 -0
- package/extensions/chrome-browser/background.mjs +36 -4
- package/extensions/chrome-browser/icons/icon128.png +0 -0
- package/extensions/chrome-browser/icons/icon16.png +0 -0
- package/extensions/chrome-browser/icons/icon48.png +0 -0
- package/extensions/chrome-browser/icons/logo.svg +39 -8
- package/extensions/chrome-browser/manifest.json +3 -3
- package/extensions/chrome-browser/popup.html +5 -1
- package/extensions/chrome-browser/popup.js +13 -0
- package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
- package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
- package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +3 -0
- package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
- package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
- package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
- package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
- package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
- package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
- package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
- package/flutter_app/lib/main.dart +2 -0
- package/flutter_app/lib/main_account_settings.dart +10 -34
- package/flutter_app/lib/main_admin.dart +14 -1
- package/flutter_app/lib/main_app_shell.dart +377 -340
- package/flutter_app/lib/main_chat.dart +707 -227
- package/flutter_app/lib/main_controller.dart +197 -42
- package/flutter_app/lib/main_devices.dart +436 -4
- package/flutter_app/lib/main_integrations.dart +255 -6
- package/flutter_app/lib/main_launcher.dart +1 -1
- package/flutter_app/lib/main_model_picker.dart +685 -0
- package/flutter_app/lib/main_models.dart +212 -0
- package/flutter_app/lib/main_navigation.dart +1 -9
- package/flutter_app/lib/main_operations.dart +1417 -614
- package/flutter_app/lib/main_settings.dart +673 -871
- package/flutter_app/lib/main_shared.dart +971 -443
- package/flutter_app/lib/main_spacing.dart +4 -4
- package/flutter_app/lib/main_theme.dart +22 -14
- package/flutter_app/lib/main_unified.dart +5 -72
- package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
- package/flutter_app/lib/src/backend_client.dart +46 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +30 -7
- package/flutter_app/lib/src/desktop_companion_io.dart +71 -1
- package/flutter_app/lib/src/security/password_strength.dart +84 -0
- package/flutter_app/lib/src/stream_renderer.dart +42 -3
- package/flutter_app/lib/src/theme/palette.dart +76 -34
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/pubspec.lock +2 -2
- package/flutter_app/pubspec.yaml +7 -1
- package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
- package/flutter_app/tool/generate_desktop_branding.py +154 -152
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +40 -9
- package/flutter_app/web/favicon_light.svg +43 -0
- package/flutter_app/web/icons/Icon-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/windows/runner/main.cpp +7 -1
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/lib/manager.js +445 -83
- package/package.json +17 -3
- package/runtime/paths.js +3 -1
- package/server/admin/access.js +198 -0
- package/server/admin/admin.css +268 -0
- package/server/admin/admin.js +348 -0
- package/server/admin/analytics.js +128 -0
- package/server/admin/index.html +1015 -0
- package/server/admin/login.html +290 -0
- package/server/admin/logo.svg +43 -0
- package/server/admin/sql.js +134 -0
- package/server/admin/users.js +147 -0
- package/server/config/origins.js +3 -1
- package/server/db/database.js +92 -0
- package/server/db/ftsQuery.js +27 -0
- package/server/http/routes.js +1 -0
- package/server/http/static.js +23 -6
- package/server/index.js +1 -1
- package/server/middleware/adminAuth.js +48 -0
- package/server/middleware/auth.js +1 -40
- package/server/public/.last_build_id +1 -1
- package/server/public/app_icon.png +0 -0
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_512.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
- package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
- package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
- package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/web/icons/Icon-192.png +0 -0
- package/server/public/favicon.png +0 -0
- package/server/public/favicon.svg +40 -9
- package/server/public/favicon_light.svg +43 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/icons/Icon-192-light.png +0 -0
- package/server/public/icons/Icon-192.png +0 -0
- package/server/public/icons/Icon-512-light.png +0 -0
- package/server/public/icons/Icon-512.png +0 -0
- package/server/public/icons/Icon-maskable-192-light.png +0 -0
- package/server/public/icons/Icon-maskable-192.png +0 -0
- package/server/public/icons/Icon-maskable-512-light.png +0 -0
- package/server/public/icons/Icon-maskable-512.png +0 -0
- package/server/public/main.dart.js +88537 -85936
- package/server/routes/admin.js +632 -0
- package/server/routes/agent_profiles.js +3 -0
- package/server/routes/agents.js +1 -1
- package/server/routes/auth.js +3 -1
- package/server/routes/mcp.js +29 -13
- package/server/routes/memory.js +23 -1
- package/server/routes/messaging.js +2 -0
- package/server/routes/screenHistory.js +14 -8
- package/server/routes/settings.js +10 -2
- package/server/routes/stream.js +1 -1
- package/server/routes/triggers.js +4 -4
- package/server/routes/voice_assistant.js +36 -1
- package/server/routes/workspace.js +86 -0
- package/server/services/account/admin_two_factor.js +132 -0
- package/server/services/account/password_policy.js +6 -1
- package/server/services/ai/completion.js +44 -0
- package/server/services/ai/engine.js +321 -500
- package/server/services/ai/imageAnalysis.js +9 -5
- package/server/services/ai/logFormat.js +46 -0
- package/server/services/ai/loopPolicy.js +11 -0
- package/server/services/ai/messagingFallback.js +228 -0
- package/server/services/ai/models.js +194 -55
- package/server/services/ai/providerRetry.js +169 -0
- package/server/services/ai/providers/anthropic.js +15 -4
- package/server/services/ai/providers/google.js +38 -11
- package/server/services/ai/providers/grok.js +19 -68
- package/server/services/ai/providers/grokOauth.js +141 -0
- package/server/services/ai/providers/nvidia.js +154 -0
- package/server/services/ai/providers/ollama.js +76 -39
- package/server/services/ai/providers/openai.js +20 -31
- package/server/services/ai/providers/openaiCodex.js +6 -2
- package/server/services/ai/providers/openaiCompatible.js +70 -0
- package/server/services/ai/providers/openrouter.js +162 -0
- package/server/services/ai/settings.js +30 -0
- package/server/services/ai/systemPrompt.js +51 -29
- package/server/services/ai/taskAnalysis.js +60 -1
- package/server/services/ai/toolEvidence.js +207 -0
- package/server/services/ai/toolSelector.js +8 -1
- package/server/services/ai/tools.js +78 -13
- package/server/services/browser/extension/registry.js +94 -9
- package/server/services/desktop/registry.js +104 -1
- package/server/services/desktop/screenRecorder.js +208 -98
- package/server/services/desktop/screen_recorder_support.js +46 -0
- package/server/services/integrations/google/provider.js +13 -0
- package/server/services/integrations/home_assistant/constants.js +88 -0
- package/server/services/integrations/home_assistant/network.js +207 -0
- package/server/services/integrations/home_assistant/provider.js +249 -0
- package/server/services/integrations/home_assistant/snapshot.js +98 -0
- package/server/services/integrations/home_assistant/tools.js +101 -0
- package/server/services/integrations/manager.js +5 -0
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/trello/provider.js +8 -2
- package/server/services/manager.js +51 -53
- package/server/services/mcp/client.js +208 -268
- package/server/services/mcp/client_support.js +172 -0
- package/server/services/mcp/recovery.js +116 -0
- package/server/services/mcp/tool_operations.js +123 -0
- package/server/services/memory/ingestion.js +185 -370
- package/server/services/memory/ingestion_coverage.js +129 -0
- package/server/services/memory/ingestion_documents.js +123 -0
- package/server/services/memory/ingestion_support.js +171 -0
- package/server/services/memory/intelligence.js +181 -0
- package/server/services/memory/manager.js +476 -32
- package/server/services/messaging/automation.js +71 -134
- package/server/services/messaging/formatting_guides.js +26 -1
- package/server/services/messaging/inbound_queue.js +111 -0
- package/server/services/messaging/manager.js +25 -8
- package/server/services/messaging/typing_keepalive.js +110 -0
- package/server/services/streaming/android-stream.js +293 -40
- package/server/services/tasks/integration_runtime.js +4 -1
- package/server/services/tasks/runtime.js +415 -56
- package/server/services/tasks/task_repository.js +48 -6
- package/server/services/websocket.js +8 -2
- package/server/services/widgets/service.js +17 -1
- package/server/services/workspace/manager.js +116 -0
- package/server/utils/logger.js +44 -6
- package/server/utils/security.js +3 -0
- package/server/utils/version.js +29 -19
- package/server/services/memory/openhuman_uplift.test.js +0 -98
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const OpenAI = require('openai');
|
|
2
|
+
const { OpenAICompatibleProvider } = require('./openaiCompatible');
|
|
3
|
+
|
|
4
|
+
const NVIDIA_BASE_URL = 'https://integrate.api.nvidia.com/v1';
|
|
5
|
+
|
|
6
|
+
// Context windows per model (tokens)
|
|
7
|
+
const CONTEXT_WINDOWS = {
|
|
8
|
+
'nvidia/nemotron-3-super-120b-a12b': 262144,
|
|
9
|
+
'moonshotai/kimi-k2.5': 262144,
|
|
10
|
+
'minimaxai/minimax-m2.5': 196608,
|
|
11
|
+
'z-ai/glm5': 202752,
|
|
12
|
+
'meta/llama-4-maverick-17b-128e-instruct': 1048576,
|
|
13
|
+
'meta/llama-4-scout-17b-16e-instruct': 1048576,
|
|
14
|
+
'deepseek-ai/deepseek-r1-0528': 163840,
|
|
15
|
+
'qwen/qwq-32b': 131072,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Reasoning models: no temperature, no top_p
|
|
19
|
+
const REASONING_MODELS = new Set([
|
|
20
|
+
'deepseek-ai/deepseek-r1-0528',
|
|
21
|
+
'qwen/qwq-32b',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
class NvidiaProvider extends OpenAICompatibleProvider {
|
|
25
|
+
constructor(config = {}) {
|
|
26
|
+
super(config);
|
|
27
|
+
this.name = 'nvidia';
|
|
28
|
+
this.models = Object.keys(CONTEXT_WINDOWS);
|
|
29
|
+
this.client = new OpenAI({
|
|
30
|
+
apiKey: config.apiKey || process.env.NVIDIA_API_KEY,
|
|
31
|
+
baseURL: config.baseUrl || NVIDIA_BASE_URL,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async listModels() {
|
|
36
|
+
try {
|
|
37
|
+
const res = await this.client.models.list();
|
|
38
|
+
const DROP = /embed|bge|e5-|rerank|guard|safety|moderat|diffus|flux|stable|imagen|vision-enc|whisper|tts|speech|paraphrase|classif/i;
|
|
39
|
+
return res.data
|
|
40
|
+
.filter((m) => !DROP.test(m.id))
|
|
41
|
+
.map((m) => ({ id: m.id, name: m.id }));
|
|
42
|
+
} catch (err) {
|
|
43
|
+
throw new Error(`NVIDIA NIM request failed: ${err?.message || String(err)}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getContextWindow(model) {
|
|
48
|
+
return CONTEXT_WINDOWS[model] ?? 131072;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_isReasoningModel(model) {
|
|
52
|
+
return REASONING_MODELS.has(model);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_buildParams(model, messages, tools, options) {
|
|
56
|
+
const params = {
|
|
57
|
+
model,
|
|
58
|
+
messages,
|
|
59
|
+
max_tokens: options.maxTokens || 8192,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (!this._isReasoningModel(model)) {
|
|
63
|
+
params.temperature = options.temperature ?? 0.6;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (tools && tools.length > 0) {
|
|
67
|
+
params.tools = this.formatTools(tools);
|
|
68
|
+
params.tool_choice = 'auto';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return params;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async chat(messages, tools = [], options = {}) {
|
|
75
|
+
const model = options.model || this.getDefaultModel();
|
|
76
|
+
const params = this._buildParams(model, messages, tools, options);
|
|
77
|
+
let response;
|
|
78
|
+
try {
|
|
79
|
+
response = await this.client.chat.completions.create(params);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
throw new Error(`NVIDIA NIM request failed: ${err?.message || String(err)}`);
|
|
82
|
+
}
|
|
83
|
+
return this.normalizeResponse(response);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async *stream(messages, tools = [], options = {}) {
|
|
87
|
+
const model = options.model || this.getDefaultModel();
|
|
88
|
+
const params = {
|
|
89
|
+
...this._buildParams(model, messages, tools, options),
|
|
90
|
+
stream: true,
|
|
91
|
+
stream_options: { include_usage: true },
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
let stream;
|
|
95
|
+
try {
|
|
96
|
+
stream = await this.client.chat.completions.create(params);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
throw new Error(`NVIDIA NIM request failed: ${err?.message || String(err)}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let toolCalls = [];
|
|
102
|
+
let content = '';
|
|
103
|
+
let finalUsage = null;
|
|
104
|
+
|
|
105
|
+
for await (const chunk of stream) {
|
|
106
|
+
if (chunk.usage && (!chunk.choices || chunk.choices.length === 0)) {
|
|
107
|
+
finalUsage = this.normalizeUsage(chunk.usage);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
112
|
+
if (!delta) continue;
|
|
113
|
+
|
|
114
|
+
if (delta.content) {
|
|
115
|
+
content += delta.content;
|
|
116
|
+
yield { type: 'content', content: delta.content };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (delta.tool_calls) {
|
|
120
|
+
for (const tc of delta.tool_calls) {
|
|
121
|
+
if (!toolCalls[tc.index]) {
|
|
122
|
+
toolCalls[tc.index] = {
|
|
123
|
+
id: tc.id || '',
|
|
124
|
+
type: 'function',
|
|
125
|
+
function: { name: tc.function?.name || '', arguments: '' },
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (tc.id) toolCalls[tc.index].id = tc.id;
|
|
129
|
+
if (tc.function?.name) toolCalls[tc.index].function.name = tc.function.name;
|
|
130
|
+
if (tc.function?.arguments) toolCalls[tc.index].function.arguments += tc.function.arguments;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const finishReason = chunk.choices[0]?.finish_reason;
|
|
135
|
+
if (finishReason === 'tool_calls' || (finishReason === 'stop' && toolCalls.length > 0)) {
|
|
136
|
+
yield { type: 'tool_calls', toolCalls, content, usage: this.normalizeUsage(chunk.usage) || finalUsage };
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (finishReason === 'stop') {
|
|
140
|
+
yield { type: 'done', content, usage: this.normalizeUsage(chunk.usage) || finalUsage };
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (toolCalls.length > 0) {
|
|
146
|
+
yield { type: 'tool_calls', toolCalls, content, usage: finalUsage };
|
|
147
|
+
} else {
|
|
148
|
+
yield { type: 'done', content, usage: finalUsage };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = { NvidiaProvider };
|
|
@@ -81,33 +81,78 @@ class OllamaProvider extends BaseProvider {
|
|
|
81
81
|
}));
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
const model = options.model || this.config.model || 'llama3.1';
|
|
86
|
-
await this.ensureModel(model);
|
|
84
|
+
buildChatBody(messages, tools, options, stream) {
|
|
87
85
|
const body = {
|
|
88
|
-
model,
|
|
89
|
-
messages: messages.map(m =>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
86
|
+
model: options.model || this.config.model || 'llama3.1',
|
|
87
|
+
messages: messages.map(m => {
|
|
88
|
+
const msg = {
|
|
89
|
+
role: m.role,
|
|
90
|
+
content: m.content || '',
|
|
91
|
+
...(m.tool_call_id ? { tool_call_id: m.tool_call_id } : {})
|
|
92
|
+
};
|
|
93
|
+
if (m.tool_calls) {
|
|
94
|
+
msg.tool_calls = m.tool_calls.map(tc => ({
|
|
95
|
+
...tc,
|
|
96
|
+
function: {
|
|
97
|
+
...tc.function,
|
|
98
|
+
arguments: typeof tc.function.arguments === 'string'
|
|
99
|
+
? (function() { try { return JSON.parse(tc.function.arguments || '{}'); } catch(e) { return {}; } })()
|
|
100
|
+
: tc.function.arguments
|
|
101
|
+
}
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
return msg;
|
|
105
|
+
}),
|
|
106
|
+
stream,
|
|
96
107
|
options: {
|
|
97
108
|
temperature: options.temperature ?? 0.7,
|
|
98
109
|
num_predict: options.maxTokens || 16384
|
|
99
110
|
}
|
|
100
111
|
};
|
|
101
|
-
|
|
102
112
|
if (tools.length > 0) {
|
|
103
113
|
body.tools = this.formatToolsForOllama(tools);
|
|
104
114
|
}
|
|
115
|
+
return body;
|
|
116
|
+
}
|
|
105
117
|
|
|
118
|
+
// Ollama returns HTTP 200 with an error body for some failures and a non-2xx
|
|
119
|
+
// status for others; surface both as real errors instead of letting callers
|
|
120
|
+
// see a silently empty response. Tags models that reject tools so the caller
|
|
121
|
+
// can transparently retry without them.
|
|
122
|
+
async postChat(body) {
|
|
106
123
|
const res = await fetch(`${this.baseUrl}/api/chat`, {
|
|
107
124
|
method: 'POST',
|
|
108
125
|
headers: { 'Content-Type': 'application/json' },
|
|
109
126
|
body: JSON.stringify(body)
|
|
110
127
|
});
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
const detail = await res.text().catch(() => '');
|
|
130
|
+
let message = detail;
|
|
131
|
+
try { message = JSON.parse(detail)?.error || detail; } catch {}
|
|
132
|
+
const err = new Error(`Ollama /api/chat failed (HTTP ${res.status}): ${message || res.statusText}`);
|
|
133
|
+
if (/does not support tools|tools.*not supported/i.test(message)) {
|
|
134
|
+
err.code = 'OLLAMA_TOOLS_UNSUPPORTED';
|
|
135
|
+
}
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
return res;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async chat(messages, tools = [], options = {}) {
|
|
142
|
+
const model = options.model || this.config.model || 'llama3.1';
|
|
143
|
+
await this.ensureModel(model);
|
|
144
|
+
|
|
145
|
+
let res;
|
|
146
|
+
try {
|
|
147
|
+
res = await this.postChat(this.buildChatBody(messages, tools, { ...options, model }, false));
|
|
148
|
+
} catch (err) {
|
|
149
|
+
if (err.code === 'OLLAMA_TOOLS_UNSUPPORTED' && tools.length > 0) {
|
|
150
|
+
console.warn(`[Ollama] Model '${model}' does not support tools; retrying without them.`);
|
|
151
|
+
res = await this.postChat(this.buildChatBody(messages, [], { ...options, model }, false));
|
|
152
|
+
} else {
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
111
156
|
|
|
112
157
|
const data = await res.json();
|
|
113
158
|
const msg = data.message || {};
|
|
@@ -135,35 +180,24 @@ class OllamaProvider extends BaseProvider {
|
|
|
135
180
|
async *stream(messages, tools = [], options = {}) {
|
|
136
181
|
const model = options.model || this.config.model || 'llama3.1';
|
|
137
182
|
await this.ensureModel(model);
|
|
138
|
-
const body = {
|
|
139
|
-
model,
|
|
140
|
-
messages: messages.map(m => ({
|
|
141
|
-
role: m.role,
|
|
142
|
-
content: m.content || '',
|
|
143
|
-
...(m.tool_calls ? { tool_calls: m.tool_calls } : {}),
|
|
144
|
-
...(m.tool_call_id ? { tool_call_id: m.tool_call_id } : {})
|
|
145
|
-
})),
|
|
146
|
-
stream: true,
|
|
147
|
-
options: {
|
|
148
|
-
temperature: options.temperature ?? 0.7,
|
|
149
|
-
num_predict: options.maxTokens || 16384
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
183
|
|
|
153
|
-
|
|
154
|
-
|
|
184
|
+
let res;
|
|
185
|
+
try {
|
|
186
|
+
res = await this.postChat(this.buildChatBody(messages, tools, { ...options, model }, true));
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (err.code === 'OLLAMA_TOOLS_UNSUPPORTED' && tools.length > 0) {
|
|
189
|
+
console.warn(`[Ollama] Model '${model}' does not support tools; retrying stream without them.`);
|
|
190
|
+
res = await this.postChat(this.buildChatBody(messages, [], { ...options, model }, true));
|
|
191
|
+
} else {
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
155
194
|
}
|
|
156
195
|
|
|
157
|
-
const res = await fetch(`${this.baseUrl}/api/chat`, {
|
|
158
|
-
method: 'POST',
|
|
159
|
-
headers: { 'Content-Type': 'application/json' },
|
|
160
|
-
body: JSON.stringify(body)
|
|
161
|
-
});
|
|
162
|
-
|
|
163
196
|
const reader = res.body.getReader();
|
|
164
197
|
const decoder = new TextDecoder();
|
|
165
198
|
let content = '';
|
|
166
199
|
let buffer = '';
|
|
200
|
+
let accumulatedToolCalls = [];
|
|
167
201
|
|
|
168
202
|
while (true) {
|
|
169
203
|
const { done, value } = await reader.read();
|
|
@@ -181,20 +215,23 @@ class OllamaProvider extends BaseProvider {
|
|
|
181
215
|
content += data.message.content;
|
|
182
216
|
yield { type: 'content', content: data.message.content };
|
|
183
217
|
}
|
|
184
|
-
if (data.
|
|
185
|
-
const
|
|
218
|
+
if (data.message?.tool_calls && Array.isArray(data.message.tool_calls)) {
|
|
219
|
+
const mapped = data.message.tool_calls.map((tc, i) => ({
|
|
186
220
|
id: `call_ollama_${Date.now()}_${i}`,
|
|
187
221
|
type: 'function',
|
|
188
222
|
function: {
|
|
189
223
|
name: tc.function.name,
|
|
190
|
-
arguments: JSON.stringify(tc.function.arguments || {})
|
|
224
|
+
arguments: typeof tc.function.arguments === 'string' ? tc.function.arguments : JSON.stringify(tc.function.arguments || {})
|
|
191
225
|
}
|
|
192
226
|
}));
|
|
227
|
+
accumulatedToolCalls = accumulatedToolCalls.concat(mapped);
|
|
228
|
+
}
|
|
229
|
+
if (data.done) {
|
|
193
230
|
yield {
|
|
194
231
|
type: 'done',
|
|
195
232
|
content,
|
|
196
|
-
toolCalls,
|
|
197
|
-
finishReason:
|
|
233
|
+
toolCalls: accumulatedToolCalls,
|
|
234
|
+
finishReason: accumulatedToolCalls.length > 0 ? 'tool_calls' : 'stop',
|
|
198
235
|
usage: data.prompt_eval_count ? {
|
|
199
236
|
promptTokens: data.prompt_eval_count || 0,
|
|
200
237
|
completionTokens: data.eval_count || 0,
|
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
const OpenAI = require('openai');
|
|
2
|
-
const {
|
|
2
|
+
const { OpenAICompatibleProvider } = require('./openaiCompatible');
|
|
3
3
|
|
|
4
|
-
class OpenAIProvider extends
|
|
4
|
+
class OpenAIProvider extends OpenAICompatibleProvider {
|
|
5
5
|
constructor(config = {}) {
|
|
6
6
|
super(config);
|
|
7
7
|
this.name = 'openai';
|
|
8
8
|
this.models = [
|
|
9
|
+
'gpt-5.5',
|
|
9
10
|
'gpt-5',
|
|
10
11
|
'gpt-5-mini',
|
|
11
12
|
'gpt-5-nano',
|
|
12
13
|
'gpt-5.2',
|
|
13
|
-
'gpt-4.1',
|
|
14
14
|
'o3',
|
|
15
|
+
'o3-pro',
|
|
15
16
|
'o4-mini'
|
|
16
17
|
];
|
|
17
18
|
// Reasoning models: no temperature, use max_completion_tokens, support reasoning_effort
|
|
18
|
-
this.reasoningModels = new Set(['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5.2', 'gpt-5.1', 'o1', 'o3', 'o4-mini', 'o3-mini']);
|
|
19
|
+
this.reasoningModels = new Set(['gpt-5.5', 'gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5.2', 'gpt-5.1', 'o1', 'o3', 'o3-pro', 'o4-mini', 'o3-mini']);
|
|
19
20
|
this.contextWindows = {
|
|
21
|
+
'gpt-5.5': 1000000,
|
|
20
22
|
'gpt-5': 400000,
|
|
21
|
-
'gpt-5-mini':
|
|
23
|
+
'gpt-5-mini': 400000,
|
|
22
24
|
'gpt-5-nano': 128000,
|
|
23
25
|
'gpt-5.2': 400000,
|
|
24
26
|
'gpt-5.1': 400000,
|
|
25
|
-
'gpt-4.1': 1047576,
|
|
26
27
|
'o3': 200000,
|
|
28
|
+
'o3-pro': 200000,
|
|
27
29
|
'o4-mini': 200000,
|
|
28
30
|
'o3-mini': 200000
|
|
29
31
|
};
|
|
@@ -34,6 +36,18 @@ class OpenAIProvider extends BaseProvider {
|
|
|
34
36
|
});
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
async listModels() {
|
|
40
|
+
try {
|
|
41
|
+
const res = await this.client.models.list();
|
|
42
|
+
const DROP = /dall-e|whisper|tts|embed|moderat|realtime|audio|transcribe|search-api|-image-|babbage|davinci-002|^sora|-instruct/i;
|
|
43
|
+
return res.data
|
|
44
|
+
.filter((m) => !DROP.test(m.id))
|
|
45
|
+
.map((m) => ({ id: m.id, name: m.id }));
|
|
46
|
+
} catch (err) {
|
|
47
|
+
throw new Error(`Failed to list OpenAI models: ${err.message || String(err)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
37
51
|
isReasoningModel(model) {
|
|
38
52
|
// Match exact IDs and prefix variants (gpt-5-2025-08-07 etc)
|
|
39
53
|
for (const id of this.reasoningModels) {
|
|
@@ -173,31 +187,6 @@ class OpenAIProvider extends BaseProvider {
|
|
|
173
187
|
}
|
|
174
188
|
}
|
|
175
189
|
|
|
176
|
-
async analyzeImage(options = {}) {
|
|
177
|
-
const model = options.model || this.getDefaultVisionModel();
|
|
178
|
-
const b64 = BaseProvider.readImageAsBase64(options.imagePath);
|
|
179
|
-
const response = await this.client.chat.completions.create({
|
|
180
|
-
model,
|
|
181
|
-
max_tokens: options.maxTokens || 4096,
|
|
182
|
-
messages: [{
|
|
183
|
-
role: 'user',
|
|
184
|
-
content: [
|
|
185
|
-
{ type: 'text', text: options.question || 'Describe this image in detail.' },
|
|
186
|
-
{
|
|
187
|
-
type: 'image_url',
|
|
188
|
-
image_url: {
|
|
189
|
-
url: `data:${options.mimeType || 'image/jpeg'};base64,${b64}`
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
]
|
|
193
|
-
}]
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
content: response.choices[0]?.message?.content || '',
|
|
198
|
-
model: response.model || model,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
190
|
}
|
|
202
191
|
|
|
203
192
|
module.exports = { OpenAIProvider };
|
|
@@ -340,8 +340,12 @@ class OpenAICodexProvider extends BaseProvider {
|
|
|
340
340
|
request.tool_choice = 'auto';
|
|
341
341
|
}
|
|
342
342
|
request.parallel_tool_calls = false;
|
|
343
|
-
//
|
|
344
|
-
|
|
343
|
+
// Reasoning parameters are only sent when there are no tools: on the Codex
|
|
344
|
+
// backend, combining reasoning mode with function-calling causes the model to
|
|
345
|
+
// produce only internal reasoning output (no function_calls, no text), which
|
|
346
|
+
// makes the engine see steps=0 and finalResponse=no on every run.
|
|
347
|
+
const hasTools = tools && tools.length > 0;
|
|
348
|
+
if (this._isReasoningModel(model) && !hasTools) {
|
|
345
349
|
const effort = options.reasoningEffort || options.reasoning_effort || 'medium';
|
|
346
350
|
request.reasoning = { effort, summary: 'auto' };
|
|
347
351
|
request.include = ['reasoning.encrypted_content'];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const { BaseProvider } = require('./base');
|
|
2
|
+
|
|
3
|
+
// Shared base for providers that speak the OpenAI Chat Completions wire format
|
|
4
|
+
// (OpenAI, Grok, NVIDIA NIM, GitHub Copilot, ...). It owns the response/usage
|
|
5
|
+
// normalization and vision request that were previously copy-pasted into each
|
|
6
|
+
// provider. Per-provider concerns — client construction, model lists, context
|
|
7
|
+
// windows, reasoning detection, and the streaming loop — stay in the subclasses,
|
|
8
|
+
// since those genuinely differ between vendors.
|
|
9
|
+
class OpenAICompatibleProvider extends BaseProvider {
|
|
10
|
+
normalizeUsage(usage) {
|
|
11
|
+
if (!usage) return null;
|
|
12
|
+
return {
|
|
13
|
+
promptTokens: usage.prompt_tokens ?? usage.promptTokens ?? 0,
|
|
14
|
+
completionTokens: usage.completion_tokens ?? usage.completionTokens ?? 0,
|
|
15
|
+
totalTokens: usage.total_tokens ?? usage.totalTokens ?? 0,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
normalizeResponse(response) {
|
|
20
|
+
const choice = response?.choices?.[0];
|
|
21
|
+
if (!choice) {
|
|
22
|
+
throw new Error(`Provider '${this.name}' returned no choices in the response`);
|
|
23
|
+
}
|
|
24
|
+
const msg = choice.message || {};
|
|
25
|
+
return {
|
|
26
|
+
content: msg.content || '',
|
|
27
|
+
toolCalls: (msg.tool_calls || [])
|
|
28
|
+
.filter((tc) => tc?.function)
|
|
29
|
+
.map((tc) => ({
|
|
30
|
+
id: tc.id,
|
|
31
|
+
type: 'function',
|
|
32
|
+
function: { name: tc.function.name, arguments: tc.function.arguments },
|
|
33
|
+
})),
|
|
34
|
+
finishReason: choice.finish_reason,
|
|
35
|
+
usage: this.normalizeUsage(response.usage),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async analyzeImage(options = {}) {
|
|
40
|
+
if (!this.supportsVision()) {
|
|
41
|
+
throw new Error(`Provider '${this.name}' does not support image analysis`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const model = options.model || this.getDefaultVisionModel();
|
|
45
|
+
const b64 = BaseProvider.readImageAsBase64(options.imagePath);
|
|
46
|
+
const response = await this.client.chat.completions.create({
|
|
47
|
+
model,
|
|
48
|
+
max_tokens: options.maxTokens || 4096,
|
|
49
|
+
messages: [{
|
|
50
|
+
role: 'user',
|
|
51
|
+
content: [
|
|
52
|
+
{ type: 'text', text: options.question || 'Describe this image in detail.' },
|
|
53
|
+
{
|
|
54
|
+
type: 'image_url',
|
|
55
|
+
image_url: {
|
|
56
|
+
url: `data:${options.mimeType || 'image/jpeg'};base64,${b64}`,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
}],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
content: response.choices[0]?.message?.content || '',
|
|
65
|
+
model: response.model || model,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { OpenAICompatibleProvider };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const OpenAI = require('openai');
|
|
2
|
+
const { OpenAICompatibleProvider } = require('./openaiCompatible');
|
|
3
|
+
|
|
4
|
+
const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
|
|
5
|
+
|
|
6
|
+
// Context windows fetched from the API are cached here so getContextWindow
|
|
7
|
+
// can serve them without a network call at inference time.
|
|
8
|
+
const contextWindowCache = new Map();
|
|
9
|
+
|
|
10
|
+
class OpenRouterProvider extends OpenAICompatibleProvider {
|
|
11
|
+
constructor(config = {}) {
|
|
12
|
+
super(config);
|
|
13
|
+
this.name = 'openrouter';
|
|
14
|
+
this.models = [];
|
|
15
|
+
this.baseURL = config.baseUrl || OPENROUTER_BASE_URL;
|
|
16
|
+
this.client = new OpenAI({
|
|
17
|
+
apiKey: config.apiKey || process.env.OPENROUTER_API_KEY,
|
|
18
|
+
baseURL: this.baseURL,
|
|
19
|
+
timeout: 90_000, // 90 s — free models can queue; avoids hanging forever
|
|
20
|
+
maxRetries: 0, // engine handles retries; avoid SDK silently re-queuing slow models
|
|
21
|
+
defaultHeaders: {
|
|
22
|
+
'HTTP-Referer': 'https://github.com/NeoLabs-Systems/NeoAgent',
|
|
23
|
+
'X-Title': 'NeoAgent',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async listModels() {
|
|
29
|
+
const res = await fetch(`${this.baseURL}/models`, {
|
|
30
|
+
headers: { 'Authorization': `Bearer ${this.client.apiKey}` },
|
|
31
|
+
});
|
|
32
|
+
if (!res.ok) throw new Error(`OpenRouter /models returned HTTP ${res.status}`);
|
|
33
|
+
const { data } = await res.json();
|
|
34
|
+
const models = data || [];
|
|
35
|
+
for (const m of models) {
|
|
36
|
+
if (m.context_length) contextWindowCache.set(m.id, m.context_length);
|
|
37
|
+
}
|
|
38
|
+
this.models = models.map((m) => m.id);
|
|
39
|
+
return models;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getContextWindow(model) {
|
|
43
|
+
return contextWindowCache.get(model) ?? 128000;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_buildParams(model, messages, tools, options) {
|
|
47
|
+
const params = {
|
|
48
|
+
model,
|
|
49
|
+
messages,
|
|
50
|
+
temperature: options.temperature ?? 0.7,
|
|
51
|
+
max_tokens: options.maxTokens || 16384,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (tools && tools.length > 0) {
|
|
55
|
+
params.tools = this.formatTools(tools);
|
|
56
|
+
params.tool_choice = 'auto';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return params;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_extractOpenRouterError(obj) {
|
|
63
|
+
if (!obj) return null;
|
|
64
|
+
const e = obj.error;
|
|
65
|
+
if (!e) return null;
|
|
66
|
+
const msg = e.message || (typeof e === 'string' ? e : JSON.stringify(e));
|
|
67
|
+
return `OpenRouter: ${msg}${e.code ? ` (code ${e.code})` : ''}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async chat(messages, tools = [], options = {}) {
|
|
71
|
+
const model = options.model || this.getDefaultModel();
|
|
72
|
+
const params = this._buildParams(model, messages, tools, options);
|
|
73
|
+
let response;
|
|
74
|
+
try {
|
|
75
|
+
response = await this.client.chat.completions.create(params);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
throw new Error(`OpenRouter request failed: ${err?.message || String(err)}`);
|
|
78
|
+
}
|
|
79
|
+
// OpenRouter returns HTTP 200 even for errors (rate limits, model unavailable, etc.)
|
|
80
|
+
const orErr = this._extractOpenRouterError(response);
|
|
81
|
+
if (orErr) throw new Error(orErr);
|
|
82
|
+
if (!response?.choices?.length) {
|
|
83
|
+
throw new Error(`OpenRouter: model returned no choices (may be rate-limited or unavailable)`);
|
|
84
|
+
}
|
|
85
|
+
return this.normalizeResponse(response);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async *stream(messages, tools = [], options = {}) {
|
|
89
|
+
const model = options.model || this.getDefaultModel();
|
|
90
|
+
const params = {
|
|
91
|
+
...this._buildParams(model, messages, tools, options),
|
|
92
|
+
stream: true,
|
|
93
|
+
stream_options: { include_usage: true },
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
let stream;
|
|
97
|
+
try {
|
|
98
|
+
stream = await this.client.chat.completions.create(params);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
throw new Error(`OpenRouter request failed: ${err?.message || String(err)}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let toolCalls = [];
|
|
104
|
+
let content = '';
|
|
105
|
+
let finalUsage = null;
|
|
106
|
+
|
|
107
|
+
// The OpenAI SDK converts OpenRouter's SSE error events (data.error) into
|
|
108
|
+
// APIErrors before yielding — so we wrap the loop to add context.
|
|
109
|
+
try {
|
|
110
|
+
for await (const chunk of stream) {
|
|
111
|
+
if (chunk.usage && (!chunk.choices || chunk.choices.length === 0)) {
|
|
112
|
+
finalUsage = this.normalizeUsage(chunk.usage);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
117
|
+
if (!delta) continue;
|
|
118
|
+
|
|
119
|
+
if (delta.content) {
|
|
120
|
+
content += delta.content;
|
|
121
|
+
yield { type: 'content', content: delta.content };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (delta.tool_calls) {
|
|
125
|
+
for (const tc of delta.tool_calls) {
|
|
126
|
+
if (!toolCalls[tc.index]) {
|
|
127
|
+
toolCalls[tc.index] = {
|
|
128
|
+
id: tc.id || '',
|
|
129
|
+
type: 'function',
|
|
130
|
+
function: { name: tc.function?.name || '', arguments: '' },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (tc.id) toolCalls[tc.index].id = tc.id;
|
|
134
|
+
if (tc.function?.name) toolCalls[tc.index].function.name = tc.function.name;
|
|
135
|
+
if (tc.function?.arguments) toolCalls[tc.index].function.arguments += tc.function.arguments;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const finishReason = chunk.choices[0]?.finish_reason;
|
|
140
|
+
if (finishReason === 'tool_calls' || (finishReason === 'stop' && toolCalls.length > 0)) {
|
|
141
|
+
yield { type: 'tool_calls', toolCalls, content, usage: this.normalizeUsage(chunk.usage) || finalUsage };
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (finishReason === 'stop') {
|
|
145
|
+
yield { type: 'done', content, usage: this.normalizeUsage(chunk.usage) || finalUsage };
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch (err) {
|
|
150
|
+
// Re-throw SDK APIErrors (from OpenRouter SSE error events) with OpenRouter context.
|
|
151
|
+
throw new Error(`OpenRouter: ${err?.message || String(err)}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (toolCalls.length > 0) {
|
|
155
|
+
yield { type: 'tool_calls', toolCalls, content, usage: finalUsage };
|
|
156
|
+
} else {
|
|
157
|
+
yield { type: 'done', content, usage: finalUsage };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = { OpenRouterProvider };
|