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,3 +1,5 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
const { AnthropicProvider } = require('./providers/anthropic');
|
|
2
4
|
const { GoogleProvider } = require('./providers/google');
|
|
3
5
|
const { GrokProvider } = require('./providers/grok');
|
|
@@ -14,6 +16,19 @@ const {
|
|
|
14
16
|
getProviderConfigs,
|
|
15
17
|
getProviderSecrets,
|
|
16
18
|
} = require('./settings');
|
|
19
|
+
const {
|
|
20
|
+
createModelSelectionId,
|
|
21
|
+
modelMatchesConfiguredId,
|
|
22
|
+
toSelectableModel,
|
|
23
|
+
} = require('./model_identity');
|
|
24
|
+
const {
|
|
25
|
+
classifyPriceTier,
|
|
26
|
+
getInputCostPerM,
|
|
27
|
+
refreshOllamaModels,
|
|
28
|
+
refreshProviderModelList,
|
|
29
|
+
} = require('./model_discovery');
|
|
30
|
+
const { fetchResponseText } = require('../network/http');
|
|
31
|
+
const { createAbortError, isAbortError, throwIfAborted } = require('../../utils/abort');
|
|
17
32
|
|
|
18
33
|
const STATIC_MODELS = [
|
|
19
34
|
// — xAI OAuth — fallback entries shown when grok-oauth token is invalid/exhausted.
|
|
@@ -130,127 +145,27 @@ const PROVIDER_FACTORIES = Object.freeze({
|
|
|
130
145
|
openrouter: { Provider: OpenRouterProvider, apiKey: true, baseUrl: true },
|
|
131
146
|
});
|
|
132
147
|
|
|
133
|
-
const dynamicModelsByBaseUrl = new Map();
|
|
134
|
-
const REFRESH_INTERVAL = 30000; // 30 seconds
|
|
135
|
-
|
|
136
|
-
// Unified dynamic model cache for all API-backed providers.
|
|
137
|
-
// Keyed by `${providerId}:${apiKey.slice(0,8)}` to handle per-user keys.
|
|
138
|
-
const providerModelCache = new Map();
|
|
139
|
-
const DYNAMIC_REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
|
140
|
-
|
|
141
|
-
// Populated from OpenRouter's /models response; used to price-classify models
|
|
142
|
-
// from all providers. Keyed by both the full OpenRouter ID ("openai/gpt-5-mini")
|
|
143
|
-
// and the bare model ID ("gpt-5-mini") for cross-provider lookup.
|
|
144
|
-
const openrouterPricingCache = new Map();
|
|
145
|
-
|
|
146
148
|
// Providers whose full model list is fetched from their API at runtime.
|
|
147
149
|
// grok-oauth inherits listModels() from GrokProvider and uses the same xAI endpoint.
|
|
148
150
|
const DYNAMIC_PROVIDERS = ['openai', 'anthropic', 'google', 'nvidia', 'grok', 'grok-oauth', 'openrouter'];
|
|
149
151
|
|
|
150
|
-
function
|
|
151
|
-
const s = id.toLowerCase();
|
|
152
|
-
if (/flash|nano|lite|tiny|haiku|scout|mini(?!max)|small/.test(s)) return 'fast';
|
|
153
|
-
if (/r1|qwq|o[0-9]|reasoning|thinking/.test(s)) return 'planning';
|
|
154
|
-
if (/code|coder|starcoder|devstral|codex|codegemma/.test(s)) return 'coding';
|
|
155
|
-
return 'general';
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Pricing tiers: free=$0 cheap<$0.50/1M medium=$0.50–$5/1M expensive>$5/1M
|
|
159
|
-
// Uses live prices from openrouterPricingCache; returns null when unknown.
|
|
160
|
-
function classifyPriceTier(modelId) {
|
|
161
|
-
const costPerM = openrouterPricingCache.get(modelId);
|
|
162
|
-
if (costPerM === undefined) return null;
|
|
163
|
-
if (costPerM === 0) return 'free';
|
|
164
|
-
if (costPerM < 0.5) return 'cheap';
|
|
165
|
-
if (costPerM < 5) return 'medium';
|
|
166
|
-
return 'expensive';
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Per-provider functions that turn a raw model object from listModels() into a display label.
|
|
170
|
-
const PROVIDER_LABEL_FN = {
|
|
171
|
-
openai: (m) => `${m.id} (OpenAI)`,
|
|
172
|
-
anthropic: (m) => `${m.name || m.id} (Anthropic)`,
|
|
173
|
-
google: (m) => `${m.name || m.id} (Google)`,
|
|
174
|
-
nvidia: (m) => `${m.id} (NVIDIA NIM)`,
|
|
175
|
-
grok: (m) => `${m.id} (xAI)`,
|
|
176
|
-
openrouter:(m) => `${m.name || m.id} (OpenRouter)`,
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
async function refreshProviderModelList(providerId, apiKey, baseUrl) {
|
|
180
|
-
const cacheKey = `${providerId}:${(apiKey || '').slice(0, 8)}`;
|
|
181
|
-
const existing = providerModelCache.get(cacheKey);
|
|
182
|
-
const now = Date.now();
|
|
183
|
-
|
|
184
|
-
if (existing && now - existing.lastRefresh <= DYNAMIC_REFRESH_INTERVAL) {
|
|
185
|
-
return existing.models;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
try {
|
|
189
|
-
const factory = PROVIDER_FACTORIES[providerId];
|
|
190
|
-
const config = {};
|
|
191
|
-
if (factory.apiKey) config.apiKey = apiKey;
|
|
192
|
-
if (factory.baseUrl) config.baseUrl = baseUrl;
|
|
193
|
-
const provider = new factory.Provider(config);
|
|
194
|
-
|
|
195
|
-
const raw = await provider.listModels();
|
|
196
|
-
|
|
197
|
-
// OpenRouter returns live pricing — populate the shared cache so all
|
|
198
|
-
// other providers can resolve their price tier without a lookup table.
|
|
199
|
-
if (providerId === 'openrouter') {
|
|
200
|
-
for (const m of raw) {
|
|
201
|
-
if (m.pricing?.prompt == null) continue;
|
|
202
|
-
const inputPerM = parseFloat(m.pricing.prompt) * 1_000_000;
|
|
203
|
-
openrouterPricingCache.set(m.id, inputPerM);
|
|
204
|
-
// Also index by the bare model ID (everything after the first "/")
|
|
205
|
-
// so that e.g. "gpt-5-mini" resolves from "openai/gpt-5-mini".
|
|
206
|
-
if (m.id.includes('/')) {
|
|
207
|
-
const bareId = m.id.slice(m.id.indexOf('/') + 1);
|
|
208
|
-
if (!openrouterPricingCache.has(bareId)) {
|
|
209
|
-
openrouterPricingCache.set(bareId, inputPerM);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const labelFn = PROVIDER_LABEL_FN[providerId] || ((m) => m.id);
|
|
216
|
-
const models = raw.map((m) => ({
|
|
217
|
-
id: m.id,
|
|
218
|
-
label: labelFn(m),
|
|
219
|
-
provider: providerId,
|
|
220
|
-
purpose: inferModelPurpose(m.id),
|
|
221
|
-
}));
|
|
222
|
-
|
|
223
|
-
providerModelCache.set(cacheKey, { models, lastRefresh: now });
|
|
224
|
-
return models;
|
|
225
|
-
} catch (err) {
|
|
226
|
-
console.warn(`[Models] Failed to refresh ${providerId} models:`, err.message);
|
|
227
|
-
// Always record a lastRefresh so we don't hammer the API on every request.
|
|
228
|
-
// Permanent errors (auth/billing/credits) get a longer backoff.
|
|
229
|
-
const isPermanent = /401|403|unauthorized|forbidden|credits|spending/i.test(err.message);
|
|
230
|
-
const backoff = isPermanent ? 30 * 60 * 1000 : DYNAMIC_REFRESH_INTERVAL;
|
|
231
|
-
providerModelCache.set(cacheKey, {
|
|
232
|
-
models: existing?.models || [],
|
|
233
|
-
lastRefresh: now - DYNAMIC_REFRESH_INTERVAL + backoff,
|
|
234
|
-
});
|
|
235
|
-
return existing?.models || [];
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
async function probeOllama(baseUrl, timeoutMs = 1500) {
|
|
240
|
-
const controller = new AbortController();
|
|
241
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
152
|
+
async function probeOllama(baseUrl, timeoutMs = 1500, signal = null) {
|
|
242
153
|
try {
|
|
243
|
-
const
|
|
154
|
+
const { response, text } = await fetchResponseText(`${baseUrl}/api/tags`, {
|
|
244
155
|
method: 'GET',
|
|
245
|
-
|
|
156
|
+
maxResponseBytes: 2 * 1024 * 1024,
|
|
157
|
+
serviceName: 'Ollama health check',
|
|
158
|
+
signal,
|
|
159
|
+
timeoutMs,
|
|
246
160
|
});
|
|
247
|
-
if (!
|
|
161
|
+
if (!response.ok) {
|
|
248
162
|
return {
|
|
249
163
|
healthy: false,
|
|
250
|
-
reason: `Ollama returned HTTP ${
|
|
164
|
+
reason: `Ollama returned HTTP ${response.status}.`
|
|
251
165
|
};
|
|
252
166
|
}
|
|
253
|
-
|
|
167
|
+
let data = {};
|
|
168
|
+
try { data = JSON.parse(text || '{}'); } catch {}
|
|
254
169
|
const modelCount = Array.isArray(data?.models) ? data.models.length : 0;
|
|
255
170
|
return {
|
|
256
171
|
healthy: true,
|
|
@@ -259,12 +174,11 @@ async function probeOllama(baseUrl, timeoutMs = 1500) {
|
|
|
259
174
|
: 'Connected to Ollama, but no local models were reported.'
|
|
260
175
|
};
|
|
261
176
|
} catch (err) {
|
|
262
|
-
|
|
177
|
+
if (isAbortError(err, signal)) throw createAbortError(signal);
|
|
178
|
+
const reason = err?.code === 'HTTP_TIMEOUT'
|
|
263
179
|
? `Ollama did not respond within ${timeoutMs}ms.`
|
|
264
180
|
: `Could not reach Ollama at ${baseUrl}.`;
|
|
265
181
|
return { healthy: false, reason };
|
|
266
|
-
} finally {
|
|
267
|
-
clearTimeout(timer);
|
|
268
182
|
}
|
|
269
183
|
}
|
|
270
184
|
|
|
@@ -337,11 +251,12 @@ function getProviderCatalog(userId, agentId = null) {
|
|
|
337
251
|
});
|
|
338
252
|
}
|
|
339
253
|
|
|
340
|
-
async function getProviderHealthCatalog(userId, agentId = null) {
|
|
254
|
+
async function getProviderHealthCatalog(userId, agentId = null, options = {}) {
|
|
341
255
|
const providers = getProviderCatalog(userId, agentId);
|
|
342
256
|
const enriched = [];
|
|
343
257
|
|
|
344
258
|
for (const provider of providers) {
|
|
259
|
+
throwIfAborted(options.signal);
|
|
345
260
|
let connected = null;
|
|
346
261
|
let healthy = provider.available;
|
|
347
262
|
let degraded = false;
|
|
@@ -349,8 +264,12 @@ async function getProviderHealthCatalog(userId, agentId = null) {
|
|
|
349
264
|
let statusLabel = provider.statusLabel;
|
|
350
265
|
let availabilityReason = provider.availabilityReason;
|
|
351
266
|
|
|
352
|
-
if (provider.id === 'ollama' && provider.enabled) {
|
|
353
|
-
const probe = await probeOllama(
|
|
267
|
+
if (provider.id === 'ollama' && provider.enabled && options.probeLocal !== false) {
|
|
268
|
+
const probe = await probeOllama(
|
|
269
|
+
provider.baseUrl || AI_PROVIDER_DEFINITIONS.ollama.defaultBaseUrl,
|
|
270
|
+
1500,
|
|
271
|
+
options.signal,
|
|
272
|
+
);
|
|
354
273
|
connected = probe.healthy;
|
|
355
274
|
healthy = provider.enabled && probe.healthy;
|
|
356
275
|
degraded = provider.enabled && !probe.healthy;
|
|
@@ -393,21 +312,30 @@ async function getProviderHealthCatalog(userId, agentId = null) {
|
|
|
393
312
|
return enriched;
|
|
394
313
|
}
|
|
395
314
|
|
|
396
|
-
async function getSupportedModels(userId, agentId = null) {
|
|
397
|
-
|
|
315
|
+
async function getSupportedModels(userId, agentId = null, options = {}) {
|
|
316
|
+
throwIfAborted(options.signal);
|
|
317
|
+
const providerCatalog = options.providerCatalog
|
|
318
|
+
|| await getProviderHealthCatalog(userId, agentId, { signal: options.signal });
|
|
398
319
|
const providerById = new Map(providerCatalog.map((provider) => [provider.id, provider]));
|
|
399
320
|
|
|
400
321
|
const all = [...STATIC_MODELS];
|
|
401
|
-
const
|
|
322
|
+
const seenModelIds = new Set(
|
|
323
|
+
STATIC_MODELS.map((model) => createModelSelectionId(model.provider, model.id)),
|
|
324
|
+
);
|
|
402
325
|
|
|
403
326
|
// Ollama: dynamic list from local server
|
|
404
327
|
const ollama = providerById.get('ollama');
|
|
405
328
|
if (ollama?.available) {
|
|
406
|
-
const dynamicModels = await
|
|
329
|
+
const dynamicModels = await refreshOllamaModels({
|
|
330
|
+
baseUrl: ollama.baseUrl || AI_PROVIDER_DEFINITIONS.ollama.defaultBaseUrl,
|
|
331
|
+
Provider: OllamaProvider,
|
|
332
|
+
signal: options.signal,
|
|
333
|
+
});
|
|
407
334
|
for (const model of dynamicModels) {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
335
|
+
const selectionId = createModelSelectionId(model.provider, model.id);
|
|
336
|
+
if (seenModelIds.has(selectionId)) continue;
|
|
337
|
+
seenModelIds.add(selectionId);
|
|
338
|
+
all.push(model);
|
|
411
339
|
}
|
|
412
340
|
}
|
|
413
341
|
|
|
@@ -416,16 +344,24 @@ async function getSupportedModels(userId, agentId = null) {
|
|
|
416
344
|
.filter((id) => providerById.get(id)?.available)
|
|
417
345
|
.map(async (id) => {
|
|
418
346
|
const runtime = getProviderRuntimeConfig(userId, id, agentId);
|
|
419
|
-
return refreshProviderModelList(
|
|
347
|
+
return refreshProviderModelList({
|
|
348
|
+
providerId: id,
|
|
349
|
+
factory: PROVIDER_FACTORIES[id],
|
|
350
|
+
apiKey: runtime.apiKey,
|
|
351
|
+
baseUrl: runtime.baseUrl,
|
|
352
|
+
signal: options.signal,
|
|
353
|
+
});
|
|
420
354
|
});
|
|
421
355
|
|
|
422
356
|
const dynamicResults = await Promise.allSettled(dynamicFetches);
|
|
357
|
+
throwIfAborted(options.signal);
|
|
423
358
|
for (const result of dynamicResults) {
|
|
424
359
|
if (result.status === 'fulfilled') {
|
|
425
360
|
for (const model of result.value) {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
361
|
+
const selectionId = createModelSelectionId(model.provider, model.id);
|
|
362
|
+
if (seenModelIds.has(selectionId)) continue;
|
|
363
|
+
seenModelIds.add(selectionId);
|
|
364
|
+
all.push(model);
|
|
429
365
|
}
|
|
430
366
|
}
|
|
431
367
|
}
|
|
@@ -449,6 +385,7 @@ async function getSupportedModels(userId, agentId = null) {
|
|
|
449
385
|
}
|
|
450
386
|
|
|
451
387
|
return all.map((model) => {
|
|
388
|
+
const selectableModel = toSelectableModel(model);
|
|
452
389
|
const provider = providerById.get(model.provider);
|
|
453
390
|
// Ollama models are always local/free; all others look up the OpenRouter
|
|
454
391
|
// pricing cache (populated above by Promise.allSettled).
|
|
@@ -457,20 +394,20 @@ async function getSupportedModels(userId, agentId = null) {
|
|
|
457
394
|
: (model.priceTier ?? classifyPriceTier(model.id));
|
|
458
395
|
|
|
459
396
|
let available = provider?.available !== false;
|
|
460
|
-
if (available && globalDisabledSet
|
|
397
|
+
if (available && modelMatchesConfiguredId(selectableModel, globalDisabledSet)) {
|
|
461
398
|
available = false;
|
|
462
399
|
}
|
|
463
|
-
if (available && planAllowedModels !== null && !planAllowedModels
|
|
400
|
+
if (available && planAllowedModels !== null && !modelMatchesConfiguredId(selectableModel, planAllowedModels)) {
|
|
464
401
|
available = false;
|
|
465
402
|
}
|
|
466
403
|
|
|
467
404
|
const bareId = model.id.includes('/') ? model.id.slice(model.id.indexOf('/') + 1) : null;
|
|
468
405
|
const inputCostPerM = model.provider === 'ollama'
|
|
469
406
|
? 0
|
|
470
|
-
: (
|
|
407
|
+
: (getInputCostPerM(model.id) ?? (bareId ? getInputCostPerM(bareId) : undefined) ?? null);
|
|
471
408
|
|
|
472
409
|
return {
|
|
473
|
-
...
|
|
410
|
+
...selectableModel,
|
|
474
411
|
priceTier,
|
|
475
412
|
inputCostPerM,
|
|
476
413
|
available,
|
|
@@ -480,37 +417,6 @@ async function getSupportedModels(userId, agentId = null) {
|
|
|
480
417
|
});
|
|
481
418
|
}
|
|
482
419
|
|
|
483
|
-
async function refreshDynamicModels(baseUrl) {
|
|
484
|
-
const cacheKey = baseUrl || AI_PROVIDER_DEFINITIONS.ollama.defaultBaseUrl;
|
|
485
|
-
const existing = dynamicModelsByBaseUrl.get(cacheKey);
|
|
486
|
-
const now = Date.now();
|
|
487
|
-
|
|
488
|
-
if (existing && now - existing.lastRefresh <= REFRESH_INTERVAL) {
|
|
489
|
-
return existing.models;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
try {
|
|
493
|
-
const ollama = new OllamaProvider({ baseUrl: cacheKey });
|
|
494
|
-
const models = await ollama.listModels();
|
|
495
|
-
const normalized = models.map((name) => ({
|
|
496
|
-
id: name,
|
|
497
|
-
label: `${name} (Ollama / Local)`,
|
|
498
|
-
provider: 'ollama',
|
|
499
|
-
purpose: 'general',
|
|
500
|
-
}));
|
|
501
|
-
|
|
502
|
-
dynamicModelsByBaseUrl.set(cacheKey, {
|
|
503
|
-
models: normalized,
|
|
504
|
-
lastRefresh: now
|
|
505
|
-
});
|
|
506
|
-
return normalized;
|
|
507
|
-
} catch (err) {
|
|
508
|
-
console.warn('[Models] Failed to refresh Ollama models:', err.message);
|
|
509
|
-
const cached = dynamicModelsByBaseUrl.get(cacheKey);
|
|
510
|
-
return cached?.models || [];
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
420
|
function createProviderInstance(providerStr, userId = null, configOverrides = {}) {
|
|
515
421
|
const factory = PROVIDER_FACTORIES[providerStr];
|
|
516
422
|
if (!factory) {
|
|
@@ -537,7 +443,6 @@ function createProviderInstance(providerStr, userId = null, configOverrides = {}
|
|
|
537
443
|
module.exports = {
|
|
538
444
|
AI_PROVIDER_DEFINITIONS,
|
|
539
445
|
PROVIDER_FACTORIES,
|
|
540
|
-
SUPPORTED_MODELS: STATIC_MODELS, // Backward compatibility
|
|
541
446
|
createProviderInstance,
|
|
542
447
|
getProviderCatalog,
|
|
543
448
|
getProviderHealthCatalog,
|
|
@@ -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,166 @@
|
|
|
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
|
+
const { isModelCoolingDown } = require('./model_failure_cache');
|
|
11
|
+
|
|
12
|
+
function buildSelection(model, userId, providerConfig) {
|
|
13
|
+
return {
|
|
14
|
+
provider: createProviderInstance(model.provider, userId, providerConfig),
|
|
15
|
+
model: getRawModelId(model),
|
|
16
|
+
modelSelectionId: model.id,
|
|
17
|
+
providerName: model.provider,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function getProviderForUser(
|
|
22
|
+
userId,
|
|
23
|
+
_task = '',
|
|
24
|
+
isSubagent = false,
|
|
25
|
+
modelOverride = null,
|
|
26
|
+
providerConfig = {},
|
|
27
|
+
) {
|
|
28
|
+
const agentId = providerConfig.agentId || null;
|
|
29
|
+
const aiSettings = getAiSettings(userId, agentId);
|
|
30
|
+
const models = await getSupportedModels(userId, agentId, {
|
|
31
|
+
signal: providerConfig.signal,
|
|
32
|
+
});
|
|
33
|
+
const selectableModels = models.filter((model) => model.available !== false);
|
|
34
|
+
const readyModels = selectableModels.filter(
|
|
35
|
+
(model) => !isModelCoolingDown(userId, agentId, model.id),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (modelOverride && typeof modelOverride === 'string') {
|
|
39
|
+
const requested = resolveModelSelection(selectableModels, modelOverride);
|
|
40
|
+
if (!requested) {
|
|
41
|
+
throw new Error(`Requested model '${modelOverride.trim()}' is not available.`);
|
|
42
|
+
}
|
|
43
|
+
if (isModelCoolingDown(userId, agentId, requested.id) && readyModels.length > 0) {
|
|
44
|
+
const configuredFallback = resolveModelSelection(
|
|
45
|
+
readyModels,
|
|
46
|
+
aiSettings.fallback_model_id,
|
|
47
|
+
);
|
|
48
|
+
const fallback = configuredFallback
|
|
49
|
+
|| readyModels.find((model) => model.provider !== requested.provider)
|
|
50
|
+
|| readyModels[0];
|
|
51
|
+
providerConfig.onStatus?.({
|
|
52
|
+
phase: 'model_fallback',
|
|
53
|
+
message: `Skipping recently unavailable model ${requested.id}; using ${fallback.id}.`,
|
|
54
|
+
});
|
|
55
|
+
return buildSelection(fallback, userId, providerConfig);
|
|
56
|
+
}
|
|
57
|
+
return buildSelection(requested, userId, providerConfig);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const configuredEnabledIds = Array.isArray(aiSettings.enabled_models)
|
|
61
|
+
? aiSettings.enabled_models.map((id) => String(id).trim()).filter(Boolean)
|
|
62
|
+
: [];
|
|
63
|
+
const enabledIds = normalizeModelSelections(selectableModels, configuredEnabledIds);
|
|
64
|
+
let availableModels = configuredEnabledIds.length > 0
|
|
65
|
+
? readyModels.filter((model) => enabledIds.includes(model.id))
|
|
66
|
+
: readyModels;
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
availableModels.length === 0
|
|
70
|
+
&& configuredEnabledIds.length > 0
|
|
71
|
+
&& enabledIds.length > 0
|
|
72
|
+
) {
|
|
73
|
+
const configuredFallback = resolveModelSelection(
|
|
74
|
+
readyModels,
|
|
75
|
+
aiSettings.fallback_model_id,
|
|
76
|
+
);
|
|
77
|
+
if (configuredFallback) {
|
|
78
|
+
providerConfig.onStatus?.({
|
|
79
|
+
phase: 'model_fallback',
|
|
80
|
+
message: `Enabled models are temporarily unavailable; using ${configuredFallback.id}.`,
|
|
81
|
+
});
|
|
82
|
+
availableModels = [configuredFallback];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// If every configured model is cooling down and there is no alternative,
|
|
87
|
+
// allow one retry instead of making the installation permanently unavailable.
|
|
88
|
+
if (availableModels.length === 0 && readyModels.length === 0) {
|
|
89
|
+
availableModels = configuredEnabledIds.length > 0
|
|
90
|
+
? selectableModels.filter((model) => enabledIds.includes(model.id))
|
|
91
|
+
: selectableModels;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (availableModels.length === 0) {
|
|
95
|
+
const message = configuredEnabledIds.length > 0
|
|
96
|
+
? 'None of the enabled AI models are currently available. Check model and provider settings.'
|
|
97
|
+
: 'No AI providers are currently available. Open Settings and configure at least one provider.';
|
|
98
|
+
throw new Error(message);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const fallbackModel = availableModels[0];
|
|
102
|
+
const userSelectedDefault = isSubagent
|
|
103
|
+
? (aiSettings.default_subagent_model || 'auto')
|
|
104
|
+
: (aiSettings.default_chat_model || 'auto');
|
|
105
|
+
const smarterSelection = aiSettings.smarter_model_selector !== false
|
|
106
|
+
&& aiSettings.smarter_model_selector !== 'false';
|
|
107
|
+
|
|
108
|
+
let selectedModel = fallbackModel;
|
|
109
|
+
if (userSelectedDefault !== 'auto') {
|
|
110
|
+
selectedModel = resolveModelSelection(availableModels, userSelectedDefault) || fallbackModel;
|
|
111
|
+
const configuredDefault = resolveModelSelection(selectableModels, userSelectedDefault);
|
|
112
|
+
if (
|
|
113
|
+
configuredDefault
|
|
114
|
+
&& configuredDefault.id !== selectedModel.id
|
|
115
|
+
&& isModelCoolingDown(userId, agentId, configuredDefault.id)
|
|
116
|
+
) {
|
|
117
|
+
providerConfig.onStatus?.({
|
|
118
|
+
phase: 'model_fallback',
|
|
119
|
+
message: `Skipping recently unavailable model ${configuredDefault.id}; using ${selectedModel.id}.`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
const selectionHint = providerConfig.selectionHint && typeof providerConfig.selectionHint === 'object'
|
|
124
|
+
? providerConfig.selectionHint
|
|
125
|
+
: {};
|
|
126
|
+
const preferredPurpose = String(selectionHint.purpose || '').trim().toLowerCase();
|
|
127
|
+
const highAutonomy = selectionHint.autonomyLevel === 'high'
|
|
128
|
+
|| selectionHint.complexity === 'complex';
|
|
129
|
+
const requiredConfidence = String(selectionHint.requiredConfidence || '').trim().toLowerCase();
|
|
130
|
+
const costMode = String(selectionHint.costMode || aiSettings.cost_mode || 'balanced_auto')
|
|
131
|
+
.trim()
|
|
132
|
+
.toLowerCase();
|
|
133
|
+
const requestedPurpose = ['planning', 'coding', 'general', 'fast'].includes(preferredPurpose)
|
|
134
|
+
? preferredPurpose
|
|
135
|
+
: '';
|
|
136
|
+
const priceRank = { free: 0, cheap: 1, medium: 2, expensive: 3 };
|
|
137
|
+
const chooseForPurpose = (purpose) => {
|
|
138
|
+
const candidates = availableModels.filter((model) => model.purpose === purpose);
|
|
139
|
+
if (candidates.length === 0) return null;
|
|
140
|
+
if (['economy', 'cost_saver', 'lowest_cost'].includes(costMode)) {
|
|
141
|
+
return [...candidates].sort((left, right) => (
|
|
142
|
+
(priceRank[left.priceTier] ?? 99) - (priceRank[right.priceTier] ?? 99)
|
|
143
|
+
))[0];
|
|
144
|
+
}
|
|
145
|
+
if (['quality', 'highest_quality'].includes(costMode) || requiredConfidence === 'high') {
|
|
146
|
+
return candidates.find((model) => model.priceTier !== 'free' && model.priceTier !== 'cheap')
|
|
147
|
+
|| candidates[0];
|
|
148
|
+
}
|
|
149
|
+
return candidates[0];
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
if (smarterSelection && requestedPurpose) {
|
|
153
|
+
selectedModel = chooseForPurpose(requestedPurpose) || fallbackModel;
|
|
154
|
+
} else if (smarterSelection && highAutonomy) {
|
|
155
|
+
selectedModel = chooseForPurpose('planning') || chooseForPurpose('general') || fallbackModel;
|
|
156
|
+
} else if (isSubagent) {
|
|
157
|
+
selectedModel = chooseForPurpose('fast') || fallbackModel;
|
|
158
|
+
} else {
|
|
159
|
+
selectedModel = chooseForPurpose('general') || fallbackModel;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return buildSelection(selectedModel, userId, providerConfig);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
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
|
|