@oar-sh/oar 0.9.22 → 0.9.24
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/CHANGELOG.md +258 -0
- package/README.md +9 -9
- package/bin/oar.js +82 -4
- package/package.json +16 -15
- package/server/agent-sdk-loader.mjs +116 -0
- package/server/agent-sdks/claude/entry.mjs +4 -0
- package/server/agent-sdks/claude/package-lock.json +1438 -0
- package/server/agent-sdks/claude/package.json +13 -0
- package/server/agent-sdks/cursor/entry-sqlite.mjs +2 -0
- package/server/agent-sdks/cursor/entry.mjs +3 -0
- package/server/agent-sdks/cursor/package-lock.json +171 -0
- package/server/agent-sdks/cursor/package.json +11 -0
- package/server/agent-sdks/koffi/entry.cjs +2 -0
- package/server/agent-sdks/koffi/package-lock.json +368 -0
- package/server/agent-sdks/koffi/package.json +10 -0
- package/server/claude-worker/claude-cli-pinned-env.mjs +4 -1
- package/server/claude-worker/claude-preview-tool.mjs +5 -2
- package/server/claude-worker/claude-remote-relay-tool.mjs +5 -2
- package/server/claude-worker/claude-sdk-adapter.mjs +4 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +7 -4
- package/server/cursor-worker/cursor-sdk-adapter.mjs +5 -3
- package/server/public/app/agent-sdk-ui.js +349 -0
- package/server/public/app/api-client.js +91 -8
- package/server/public/app/bootstrap.js +204 -69
- package/server/public/app/model-refresh-ui.mjs +413 -0
- package/server/public/app/no-conversation-state.mjs +5 -2
- package/server/public/app/provider-readiness.mjs +67 -4
- package/server/public/app/settings-modal.js +167 -0
- package/server/public/app/socket-handlers.js +21 -0
- package/server/public/index.html +95 -2
- package/server/routes/agent-sdk-routes.mjs +73 -0
- package/server/routes/messages-routes.mjs +102 -37
- package/server/routes/sessions-routes.mjs +236 -110
- package/server/server-runtime.mjs +416 -121
- package/server/services/about-service.mjs +26 -6
- package/server/services/agent-sdk-launch-gate.mjs +124 -0
- package/server/services/agent-sdk-paths.mjs +307 -0
- package/server/services/agent-sdk-store.mjs +1211 -0
- package/server/services/claude-api-settings-service.mjs +54 -17
- package/server/services/cli-install-service.mjs +3 -2
- package/server/services/copilot-model-discovery-service.mjs +7 -1
- package/server/services/database-file.mjs +17 -1
- package/server/services/deepseek-settings-service.mjs +58 -20
- package/server/services/earlier-database-service.mjs +5 -3
- package/server/services/file-serving.mjs +13 -0
- package/server/services/model-refresh-scheduler.mjs +153 -0
- package/server/services/model-variant-catalog-service.mjs +141 -9
- package/server/services/native-addon-build-service.mjs +224 -0
- package/server/services/oar-cli-helpers.mjs +76 -10
- package/server/services/oar-state-migration-service.mjs +2 -12
- package/server/services/oar-state-root.mjs +17 -0
- package/server/services/provider-model-refresh.mjs +326 -0
- package/server/services/provider-model-state.mjs +395 -0
- package/server/services/relay-console-log.mjs +2 -3
- package/server/services/relay-singleton-guard.mjs +72 -10
- package/server/services/remote-relay-loopback.mjs +3 -3
- package/server/services/sdk-session-sync-service.mjs +1 -1
- package/server/services/session-worker-launch-service.mjs +13 -0
- package/server/services/session-worker-supervisor-service.mjs +19 -1
- package/server/services/session-worker-websocket-service.mjs +15 -0
- package/server/services/update-install-service.mjs +34 -3
- package/server/services/worker-launch-note-service.mjs +14 -2
- package/server/services/worker-recycle-service.mjs +145 -11
- package/shared/model-refresh-schedule.mjs +120 -0
- package/shared/worker-runtime/direct-openai-image.mjs +8 -4
- package/server/services/package-data-rescue-preinstall.mjs +0 -22
- package/server/services/package-data-rescue.mjs +0 -139
- package/server/tty-console-bootstrap.mjs +0 -75
|
@@ -155,13 +155,15 @@ import { deepseekStartingBalanceNote } from '../services/plan-usage-deepseek.mjs
|
|
|
155
155
|
import { appendTokenSections } from '../services/plan-usage-tokens.mjs';
|
|
156
156
|
import { DEEPSEEK_USAGE_CHECKPOINTS_TABLE } from '../migrations/0024-deepseek-usage-totals.mjs';
|
|
157
157
|
import { claudeProjectDirSlug } from '../services/claude-session-root-service.mjs';
|
|
158
|
+
import { buildProviderRefreshTable, refreshProviderModels } from '../services/provider-model-refresh.mjs';
|
|
159
|
+
import { MODEL_REFRESH_INTERVAL_HOURS_CHOICES, MODEL_REFRESH_PROVIDERS, DEFAULT_MODEL_REFRESH_AUTO, DEFAULT_MODEL_REFRESH_INTERVAL_HOURS } from '../../shared/model-refresh-schedule.mjs';
|
|
158
160
|
|
|
159
161
|
export { mapUsageSnapshotRow };
|
|
160
162
|
|
|
161
163
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
162
164
|
const SESSION_WORKER_STATUS_QUEUE_STATES = Object.freeze(['pending', 'processing', 'parked']);
|
|
163
165
|
// Registry statuses that mean "this worker is currently running" (same triple
|
|
164
|
-
// as
|
|
166
|
+
// as ACTIVE_SESSION_CWD_STATUSES in server-runtime.mjs).
|
|
165
167
|
const SESSION_WORKER_ACTIVE_STATUSES = Object.freeze(['starting', 'ready', 'processing']);
|
|
166
168
|
// A shared attachment's URL names its bytes (sha256 or a fixed generated-image
|
|
167
169
|
// path), so a kept answer stays right.
|
|
@@ -305,7 +307,9 @@ export function parseOpenAISettingsUpdateRequest(body = {}) {
|
|
|
305
307
|
const hasEnabled = typeof payload.enabled === 'boolean';
|
|
306
308
|
const hasBaseUrl = Object.prototype.hasOwnProperty.call(payload, 'baseUrl');
|
|
307
309
|
const baseUrl = hasBaseUrl ? String(payload.baseUrl || '').trim() : undefined;
|
|
308
|
-
|
|
310
|
+
const hasEnabledModels = Array.isArray(payload.enabledModels);
|
|
311
|
+
const hasAutoEnableNew = typeof payload.autoEnableNew === 'boolean';
|
|
312
|
+
if (!remove && !hasModel && !apiKey && !hasEnabled && !hasBaseUrl && !hasEnabledModels && !hasAutoEnableNew) {
|
|
309
313
|
return { ok: false, error: 'No OpenAI settings update provided' };
|
|
310
314
|
}
|
|
311
315
|
if (hasModel && !isSafeProviderModelId(model)) {
|
|
@@ -326,6 +330,9 @@ export function parseOpenAISettingsUpdateRequest(body = {}) {
|
|
|
326
330
|
apiKey,
|
|
327
331
|
...(hasModel ? { model } : {}),
|
|
328
332
|
...(hasBaseUrl ? { baseUrl } : {}),
|
|
333
|
+
...(hasEnabledModels ? { enabledModels: payload.enabledModels } : {}),
|
|
334
|
+
...(hasAutoEnableNew ? { autoEnableNew: payload.autoEnableNew } : {}),
|
|
335
|
+
...(Array.isArray(payload.shownModels) ? { shownModels: payload.shownModels } : {}),
|
|
329
336
|
enabled: hasEnabled ? payload.enabled : (apiKey ? true : undefined),
|
|
330
337
|
};
|
|
331
338
|
}
|
|
@@ -337,13 +344,14 @@ export function parseClaudeSettingsUpdateRequest(body = {}) {
|
|
|
337
344
|
const model = hasModel ? (String(payload.model || '').trim() || 'claude-sonnet-5') : undefined;
|
|
338
345
|
const hasModels = Array.isArray(payload.models);
|
|
339
346
|
const hasEnabledModels = Array.isArray(payload.enabledModels);
|
|
347
|
+
const hasAutoEnableNew = typeof payload.autoEnableNew === 'boolean';
|
|
340
348
|
const hasAttributionMode = Object.prototype.hasOwnProperty.call(payload, 'attributionMode');
|
|
341
349
|
const attributionMode = hasAttributionMode ? normalizeClaudeAttributionMode(payload.attributionMode) : undefined;
|
|
342
350
|
const hasBackgroundCommandLimit = Object.prototype.hasOwnProperty.call(payload, 'backgroundCommandLimitMinutes');
|
|
343
351
|
const backgroundCommandLimit = hasBackgroundCommandLimit
|
|
344
352
|
? parseClaudeBackgroundCommandLimitUpdate(payload.backgroundCommandLimitMinutes)
|
|
345
353
|
: null;
|
|
346
|
-
if (!hasEnabled && !hasModel && !hasModels && !hasEnabledModels && !hasAttributionMode && !hasBackgroundCommandLimit) {
|
|
354
|
+
if (!hasEnabled && !hasModel && !hasModels && !hasEnabledModels && !hasAutoEnableNew && !hasAttributionMode && !hasBackgroundCommandLimit) {
|
|
347
355
|
return { ok: false, error: 'No Claude settings update provided' };
|
|
348
356
|
}
|
|
349
357
|
if (backgroundCommandLimit && !backgroundCommandLimit.ok) {
|
|
@@ -361,6 +369,8 @@ export function parseClaudeSettingsUpdateRequest(body = {}) {
|
|
|
361
369
|
...(hasModel ? { model } : {}),
|
|
362
370
|
...(hasModels ? { models: payload.models } : {}),
|
|
363
371
|
...(hasEnabledModels ? { enabledModels: payload.enabledModels } : {}),
|
|
372
|
+
...(hasAutoEnableNew ? { autoEnableNew: payload.autoEnableNew } : {}),
|
|
373
|
+
...(Array.isArray(payload.shownModels) ? { shownModels: payload.shownModels } : {}),
|
|
364
374
|
...(hasAttributionMode ? { attributionMode } : {}),
|
|
365
375
|
// Clamped to 30 min … 8 h and snapped to 30-minute steps.
|
|
366
376
|
...(backgroundCommandLimit ? { backgroundCommandLimitMinutes: backgroundCommandLimit.minutes } : {}),
|
|
@@ -459,7 +469,8 @@ export function parseCursorSettingsUpdateRequest(body = {}) {
|
|
|
459
469
|
const apiKey = String(payload.apiKey || '').trim();
|
|
460
470
|
const hasEnabled = typeof payload.enabled === 'boolean';
|
|
461
471
|
const hasEnabledModels = Array.isArray(payload.enabledModels);
|
|
462
|
-
|
|
472
|
+
const hasAutoEnableNew = typeof payload.autoEnableNew === 'boolean';
|
|
473
|
+
if (!remove && !hasModel && !apiKey && !hasEnabled && !hasEnabledModels && !hasAutoEnableNew) {
|
|
463
474
|
return { ok: false, error: 'No Cursor settings update provided' };
|
|
464
475
|
}
|
|
465
476
|
if (hasModel && !isSafeCursorModelId(model)) {
|
|
@@ -480,6 +491,8 @@ export function parseCursorSettingsUpdateRequest(body = {}) {
|
|
|
480
491
|
apiKey,
|
|
481
492
|
...(hasModel ? { model } : {}),
|
|
482
493
|
...(hasEnabledModels ? { enabledModels: payload.enabledModels } : {}),
|
|
494
|
+
...(hasAutoEnableNew ? { autoEnableNew: payload.autoEnableNew } : {}),
|
|
495
|
+
...(Array.isArray(payload.shownModels) ? { shownModels: payload.shownModels } : {}),
|
|
483
496
|
enabled: hasEnabled ? payload.enabled : (apiKey ? true : undefined),
|
|
484
497
|
};
|
|
485
498
|
}
|
|
@@ -491,7 +504,8 @@ export function parseGrokSettingsUpdateRequest(body = {}) {
|
|
|
491
504
|
const model = hasModel ? (String(payload.model || '').trim() || 'grok-4.5') : undefined;
|
|
492
505
|
const hasModels = Array.isArray(payload.models);
|
|
493
506
|
const hasEnabledModels = Array.isArray(payload.enabledModels);
|
|
494
|
-
|
|
507
|
+
const hasAutoEnableNew = typeof payload.autoEnableNew === 'boolean';
|
|
508
|
+
if (!hasEnabled && !hasModel && !hasModels && !hasEnabledModels && !hasAutoEnableNew) {
|
|
495
509
|
return { ok: false, error: 'No Grok settings update provided' };
|
|
496
510
|
}
|
|
497
511
|
if (hasModel && !isSafeGrokModelId(model)) {
|
|
@@ -503,6 +517,8 @@ export function parseGrokSettingsUpdateRequest(body = {}) {
|
|
|
503
517
|
...(hasModel ? { model } : {}),
|
|
504
518
|
...(hasModels ? { models: payload.models } : {}),
|
|
505
519
|
...(hasEnabledModels ? { enabledModels: payload.enabledModels } : {}),
|
|
520
|
+
...(hasAutoEnableNew ? { autoEnableNew: payload.autoEnableNew } : {}),
|
|
521
|
+
...(Array.isArray(payload.shownModels) ? { shownModels: payload.shownModels } : {}),
|
|
506
522
|
};
|
|
507
523
|
}
|
|
508
524
|
|
|
@@ -2204,6 +2220,22 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2204
2220
|
listModelVariantRows,
|
|
2205
2221
|
refreshModelVariantCatalogFromCli,
|
|
2206
2222
|
setEnabledModelVariants,
|
|
2223
|
+
// Copilot's model map (model-variant-catalog-service.mjs) and the other
|
|
2224
|
+
// providers' (provider-model-state.mjs): the switch, the "New" flags.
|
|
2225
|
+
getCopilotModelState = () => ({ autoEnableNew: false, unseen: [] }),
|
|
2226
|
+
setCopilotAutoEnableNew = null,
|
|
2227
|
+
markCopilotModelsSeen = () => false,
|
|
2228
|
+
markProviderModelsSeen = () => false,
|
|
2229
|
+
// When each provider was last refreshed and the new models the next
|
|
2230
|
+
// models_updated carries (provider-model-refresh.mjs), the background
|
|
2231
|
+
// check's settings and the check itself. Absent in most tests.
|
|
2232
|
+
modelRefreshLedger = null,
|
|
2233
|
+
modelRefreshSettings = null,
|
|
2234
|
+
modelRefreshScheduler = null,
|
|
2235
|
+
// Whether Claude and Grok are set up for the background check (their CLI
|
|
2236
|
+
// or their login); absent, switched on is enough.
|
|
2237
|
+
isClaudeModelRefreshSetUp = () => true,
|
|
2238
|
+
isGrokModelRefreshSetUp = () => true,
|
|
2207
2239
|
SUPPORTED_REASONING_EFFORTS,
|
|
2208
2240
|
buildRelayReadyBannerData,
|
|
2209
2241
|
workspaceRootPayload,
|
|
@@ -2664,10 +2696,62 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2664
2696
|
);
|
|
2665
2697
|
}
|
|
2666
2698
|
|
|
2699
|
+
// The providers the Refresh button and the background check ask, with
|
|
2700
|
+
// their enablement and refresh functions (provider-model-refresh.mjs).
|
|
2701
|
+
const providerRefreshTable = buildProviderRefreshTable({
|
|
2702
|
+
getOpenAIProviderSettings,
|
|
2703
|
+
refreshOpenAIProviderModels,
|
|
2704
|
+
getClaudeProviderSettings,
|
|
2705
|
+
refreshClaudeProviderModels,
|
|
2706
|
+
getCursorProviderSettings,
|
|
2707
|
+
refreshCursorProviderModels,
|
|
2708
|
+
getGrokProviderSettings,
|
|
2709
|
+
refreshGrokProviderModels,
|
|
2710
|
+
deepseekSettingsService,
|
|
2711
|
+
claudeApiSettingsService,
|
|
2712
|
+
copilotModelDiscoveryService,
|
|
2713
|
+
isClaudeSetUp: isClaudeModelRefreshSetUp,
|
|
2714
|
+
isGrokSetUp: isGrokModelRefreshSetUp,
|
|
2715
|
+
});
|
|
2716
|
+
|
|
2717
|
+
/** Models flagged new on the enabled providers: the Select Models menu dot. */
|
|
2718
|
+
function countUnseenNewModels() {
|
|
2719
|
+
let count = 0;
|
|
2720
|
+
for (const settings of [
|
|
2721
|
+
getOpenAIProviderSettings(),
|
|
2722
|
+
getClaudeProviderSettings(),
|
|
2723
|
+
getCursorProviderSettings(),
|
|
2724
|
+
getGrokProviderSettings(),
|
|
2725
|
+
getDeepSeekProviderSettings(),
|
|
2726
|
+
getClaudeApiProviderSettings(),
|
|
2727
|
+
]) {
|
|
2728
|
+
if (settings?.enabled === true && Array.isArray(settings.unseenModels)) count += settings.unseenModels.length;
|
|
2729
|
+
}
|
|
2730
|
+
const copilot = getCopilotModelState();
|
|
2731
|
+
return count + (Array.isArray(copilot?.unseen) ? copilot.unseen.length : 0);
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
/**
|
|
2735
|
+
* What models_updated carries: the catalog, the menu dot's count and, once,
|
|
2736
|
+
* the models a refresh found for the first time (the toast).
|
|
2737
|
+
*/
|
|
2738
|
+
function modelsUpdatedPayload(modelState, openAISettings = getOpenAIProviderSettings()) {
|
|
2739
|
+
const newModels = modelRefreshLedger?.takeNewModels?.() || [];
|
|
2740
|
+
return {
|
|
2741
|
+
...buildModelCatalogWithProviders(modelState, openAISettings),
|
|
2742
|
+
unseenNewModelCount: countUnseenNewModels(),
|
|
2743
|
+
...(newModels.length ? { newModels } : {}),
|
|
2744
|
+
};
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2667
2747
|
// A discovery that lands outside any route (the deferred boot refresh) still
|
|
2668
2748
|
// has to reach connected pickers the way a snapshot POST does.
|
|
2669
2749
|
copilotModelDiscoveryService?.setOnCatalogUpdated?.(() => {
|
|
2670
|
-
io.emit('models_updated',
|
|
2750
|
+
io.emit('models_updated', modelsUpdatedPayload(getModelCatalogState()));
|
|
2751
|
+
});
|
|
2752
|
+
// So does a background check that changed a provider's models.
|
|
2753
|
+
modelRefreshScheduler?.setOnModelsChanged?.(() => {
|
|
2754
|
+
io.emit('models_updated', modelsUpdatedPayload(getModelCatalogState()));
|
|
2671
2755
|
});
|
|
2672
2756
|
|
|
2673
2757
|
// The client address as Express derives it through `trust proxy`: a
|
|
@@ -7152,8 +7236,12 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7152
7236
|
runtimeSessionId: runtimeSession?.id || null,
|
|
7153
7237
|
status: workerState?.status || 'ready',
|
|
7154
7238
|
});
|
|
7155
|
-
|
|
7156
|
-
|
|
7239
|
+
// A deferred start (the agent SDK still installs) has no worker to
|
|
7240
|
+
// call idle: it launches when the install ends.
|
|
7241
|
+
if (!ensureResult.deferred) {
|
|
7242
|
+
const pendingDepth = Number(queueCounts?.().pendingCount || 0);
|
|
7243
|
+
sessionWorkerSupervisor?.markIdle?.(ownerSessionId, pendingDepth);
|
|
7244
|
+
}
|
|
7157
7245
|
} else if (ownerSessionId) {
|
|
7158
7246
|
// Same state as the 409 above: the conversation is committed, only the
|
|
7159
7247
|
// worker is missing, so the client gets what it needs to open it.
|
|
@@ -7409,7 +7497,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7409
7497
|
baseUrl: String(currentSettings?.baseUrl || 'https://api.openai.com/v1').trim() || 'https://api.openai.com/v1',
|
|
7410
7498
|
reconciliation,
|
|
7411
7499
|
};
|
|
7412
|
-
io.emit('models_updated',
|
|
7500
|
+
io.emit('models_updated', modelsUpdatedPayload(
|
|
7413
7501
|
getModelCatalogState(),
|
|
7414
7502
|
currentSettings,
|
|
7415
7503
|
));
|
|
@@ -7523,7 +7611,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7523
7611
|
...claudeBackgroundCommandLimitSettingsFields(currentSettings),
|
|
7524
7612
|
reconciliation,
|
|
7525
7613
|
};
|
|
7526
|
-
io.emit('models_updated',
|
|
7614
|
+
io.emit('models_updated', modelsUpdatedPayload(getModelCatalogState()));
|
|
7527
7615
|
io.emit('claude_settings_updated', settingsPayload);
|
|
7528
7616
|
const reconciliationFailures = Array.isArray(reconciliation?.failedConversations)
|
|
7529
7617
|
? reconciliation.failedConversations
|
|
@@ -7646,7 +7734,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7646
7734
|
console.log(`[claude-auth] model refresh after login failed: ${error?.message || error}`);
|
|
7647
7735
|
}
|
|
7648
7736
|
const settings = getClaudeProviderSettings();
|
|
7649
|
-
io.emit('models_updated',
|
|
7737
|
+
io.emit('models_updated', modelsUpdatedPayload(getModelCatalogState()));
|
|
7650
7738
|
io.emit('claude_settings_updated', {
|
|
7651
7739
|
configured: settings?.configured === true,
|
|
7652
7740
|
enabled: settings?.enabled === true,
|
|
@@ -7738,7 +7826,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7738
7826
|
console.log(`[cli-install] Grok model refresh after install failed: ${error?.message || error}`);
|
|
7739
7827
|
}
|
|
7740
7828
|
const settings = getGrokProviderSettings();
|
|
7741
|
-
io.emit('models_updated',
|
|
7829
|
+
io.emit('models_updated', modelsUpdatedPayload(getModelCatalogState()));
|
|
7742
7830
|
io.emit('grok_settings_updated', {
|
|
7743
7831
|
configured: settings?.configured === true,
|
|
7744
7832
|
enabled: settings?.enabled === true,
|
|
@@ -7755,7 +7843,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7755
7843
|
console.log(`[cli-install] Claude model refresh after install failed: ${error?.message || error}`);
|
|
7756
7844
|
}
|
|
7757
7845
|
const settings = getClaudeProviderSettings();
|
|
7758
|
-
io.emit('models_updated',
|
|
7846
|
+
io.emit('models_updated', modelsUpdatedPayload(getModelCatalogState()));
|
|
7759
7847
|
io.emit('claude_settings_updated', {
|
|
7760
7848
|
configured: settings?.configured === true,
|
|
7761
7849
|
enabled: settings?.enabled === true,
|
|
@@ -7939,7 +8027,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7939
8027
|
console.log(`[grok-auth] model refresh after login failed: ${error?.message || error}`);
|
|
7940
8028
|
}
|
|
7941
8029
|
const settings = getGrokProviderSettings();
|
|
7942
|
-
io.emit('models_updated',
|
|
8030
|
+
io.emit('models_updated', modelsUpdatedPayload(getModelCatalogState()));
|
|
7943
8031
|
io.emit('grok_settings_updated', {
|
|
7944
8032
|
configured: settings?.configured === true,
|
|
7945
8033
|
enabled: settings?.enabled === true,
|
|
@@ -8040,7 +8128,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8040
8128
|
availableModels: Array.isArray(currentSettings?.availableModels) ? currentSettings.availableModels : [],
|
|
8041
8129
|
reconciliation,
|
|
8042
8130
|
};
|
|
8043
|
-
io.emit('models_updated',
|
|
8131
|
+
io.emit('models_updated', modelsUpdatedPayload(getModelCatalogState()));
|
|
8044
8132
|
io.emit('grok_settings_updated', settingsPayload);
|
|
8045
8133
|
const reconciliationFailures = Array.isArray(reconciliation?.failedConversations)
|
|
8046
8134
|
? reconciliation.failedConversations
|
|
@@ -8176,7 +8264,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8176
8264
|
availableModels: Array.isArray(currentSettings?.availableModels) ? currentSettings.availableModels : [],
|
|
8177
8265
|
reconciliation,
|
|
8178
8266
|
};
|
|
8179
|
-
io.emit('models_updated',
|
|
8267
|
+
io.emit('models_updated', modelsUpdatedPayload(getModelCatalogState()));
|
|
8180
8268
|
io.emit('cursor_settings_updated', settingsPayload);
|
|
8181
8269
|
const reconciliationFailures = Array.isArray(reconciliation?.failedConversations)
|
|
8182
8270
|
? reconciliation.failedConversations
|
|
@@ -8270,7 +8358,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8270
8358
|
failedConversations,
|
|
8271
8359
|
},
|
|
8272
8360
|
};
|
|
8273
|
-
io.emit('models_updated',
|
|
8361
|
+
io.emit('models_updated', modelsUpdatedPayload(getModelCatalogState()));
|
|
8274
8362
|
io.emit('deepseek_settings_updated', settingsPayload);
|
|
8275
8363
|
return res.json({
|
|
8276
8364
|
ok: true,
|
|
@@ -8324,7 +8412,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8324
8412
|
failedConversations,
|
|
8325
8413
|
},
|
|
8326
8414
|
};
|
|
8327
|
-
io.emit('models_updated',
|
|
8415
|
+
io.emit('models_updated', modelsUpdatedPayload(getModelCatalogState()));
|
|
8328
8416
|
io.emit('claude_api_settings_updated', settingsPayload);
|
|
8329
8417
|
try { claudeApiStopLedger?.clearAll?.(); } catch {}
|
|
8330
8418
|
try { checkClaudeApiCap('settings'); } catch {}
|
|
@@ -8910,9 +8998,19 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8910
8998
|
});
|
|
8911
8999
|
});
|
|
8912
9000
|
|
|
9001
|
+
// Each provider tab's switch, "New" tags and fallback warning.
|
|
9002
|
+
function modelStateFields(settings) {
|
|
9003
|
+
return {
|
|
9004
|
+
autoEnableNew: settings?.autoEnableNew !== false,
|
|
9005
|
+
unseen: Array.isArray(settings?.unseenModels) ? settings.unseenModels : [],
|
|
9006
|
+
fallback: settings?.fallback === true,
|
|
9007
|
+
};
|
|
9008
|
+
}
|
|
9009
|
+
|
|
8913
9010
|
function buildModelVariantCatalogPayloadForRoute() {
|
|
8914
9011
|
const rows = typeof listModelVariantRows === 'function' ? listModelVariantRows() : [];
|
|
8915
9012
|
const modelState = getModelCatalogState();
|
|
9013
|
+
const openAISettings = getOpenAIProviderSettings();
|
|
8916
9014
|
const claudeSettings = getClaudeProviderSettings();
|
|
8917
9015
|
const cursorSettings = getCursorProviderSettings();
|
|
8918
9016
|
const grokSettings = getGrokProviderSettings();
|
|
@@ -8926,11 +9024,21 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8926
9024
|
contextLimitsByModel: modelState.contextLimitsByModel || {},
|
|
8927
9025
|
modelMetadataByModel: modelState.modelMetadataByModel || {},
|
|
8928
9026
|
}),
|
|
9027
|
+
copilotModelState: getCopilotModelState(),
|
|
9028
|
+
openaiModels: openAISettings?.enabled === true
|
|
9029
|
+
? {
|
|
9030
|
+
defaultModel: openAISettings.model,
|
|
9031
|
+
availableModels: Array.isArray(openAISettings.availableModels) ? openAISettings.availableModels : [],
|
|
9032
|
+
enabledModels: Array.isArray(openAISettings.models) ? openAISettings.models : [],
|
|
9033
|
+
...modelStateFields(openAISettings),
|
|
9034
|
+
}
|
|
9035
|
+
: null,
|
|
8929
9036
|
claudeModels: claudeSettings?.enabled === true
|
|
8930
9037
|
? {
|
|
8931
9038
|
defaultModel: claudeSettings.model,
|
|
8932
9039
|
availableModels: Array.isArray(claudeSettings.availableModels) ? claudeSettings.availableModels : [],
|
|
8933
9040
|
enabledModels: Array.isArray(claudeSettings.models) ? claudeSettings.models : [],
|
|
9041
|
+
...modelStateFields(claudeSettings),
|
|
8934
9042
|
}
|
|
8935
9043
|
: null,
|
|
8936
9044
|
cursorModels: cursorSettings?.enabled === true
|
|
@@ -8938,6 +9046,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8938
9046
|
defaultModel: cursorSettings.model,
|
|
8939
9047
|
availableModels: Array.isArray(cursorSettings.availableModels) ? cursorSettings.availableModels : [],
|
|
8940
9048
|
enabledModels: Array.isArray(cursorSettings.models) ? cursorSettings.models : [],
|
|
9049
|
+
...modelStateFields(cursorSettings),
|
|
8941
9050
|
}
|
|
8942
9051
|
: null,
|
|
8943
9052
|
grokModels: grokSettings?.enabled === true
|
|
@@ -8945,6 +9054,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8945
9054
|
defaultModel: grokSettings.model,
|
|
8946
9055
|
availableModels: Array.isArray(grokSettings.availableModels) ? grokSettings.availableModels : [],
|
|
8947
9056
|
enabledModels: Array.isArray(grokSettings.models) ? grokSettings.models : [],
|
|
9057
|
+
...modelStateFields(grokSettings),
|
|
8948
9058
|
}
|
|
8949
9059
|
: null,
|
|
8950
9060
|
deepseekModels: deepseekSettings?.enabled === true
|
|
@@ -8952,6 +9062,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8952
9062
|
defaultModel: deepseekSettings.model,
|
|
8953
9063
|
availableModels: Array.isArray(deepseekSettings.availableModels) ? deepseekSettings.availableModels : [],
|
|
8954
9064
|
enabledModels: Array.isArray(deepseekSettings.models) ? deepseekSettings.models : [],
|
|
9065
|
+
...modelStateFields(deepseekSettings),
|
|
8955
9066
|
}
|
|
8956
9067
|
: null,
|
|
8957
9068
|
// The Select Models → Claude API tab: its own list, "[1m]" ids included
|
|
@@ -8962,6 +9073,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8962
9073
|
availableModels: Array.isArray(claudeApiSettings.availableModels) ? claudeApiSettings.availableModels : [],
|
|
8963
9074
|
enabledModels: Array.isArray(claudeApiSettings.models) ? claudeApiSettings.models : [],
|
|
8964
9075
|
discovered: claudeApiSettings.discovered === true,
|
|
9076
|
+
...modelStateFields(claudeApiSettings),
|
|
8965
9077
|
}
|
|
8966
9078
|
: null,
|
|
8967
9079
|
};
|
|
@@ -8978,101 +9090,28 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8978
9090
|
return res.status(501).json({ error: 'Model refresh is unavailable' });
|
|
8979
9091
|
}
|
|
8980
9092
|
try {
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
const
|
|
8985
|
-
const deepseekSettings = getDeepSeekProviderSettings();
|
|
8986
|
-
const claudeApiSettings = getClaudeApiProviderSettings();
|
|
8987
|
-
const refreshTasks = [
|
|
9093
|
+
// The help-text CLI refresh beside every enabled provider's discovery
|
|
9094
|
+
// (provider-model-refresh.mjs; Copilot discovery gates itself on an
|
|
9095
|
+
// installed CLI). Only a failed CLI refresh fails the request.
|
|
9096
|
+
const [cliRefresh, providerRefresh] = await Promise.allSettled([
|
|
8988
9097
|
refreshModelVariantCatalogFromCli(),
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
: Promise.resolve({ ok: true, skipped: true, models: [], error: null }),
|
|
8992
|
-
claudeSettings?.enabled === true
|
|
8993
|
-
? refreshClaudeProviderModels()
|
|
8994
|
-
: Promise.resolve({ ok: true, skipped: true, models: [], error: null }),
|
|
8995
|
-
cursorSettings?.enabled === true
|
|
8996
|
-
? refreshCursorProviderModels()
|
|
8997
|
-
: Promise.resolve({ ok: true, skipped: true, models: [], error: null }),
|
|
8998
|
-
grokSettings?.enabled === true
|
|
8999
|
-
? refreshGrokProviderModels()
|
|
9000
|
-
: Promise.resolve({ ok: true, skipped: true, models: [], error: null }),
|
|
9001
|
-
// Copilot SDK discovery self-gates on an installed CLI (skipped:true
|
|
9002
|
-
// when there is none), so unlike the providers above it has no
|
|
9003
|
-
// enablement flag to check here.
|
|
9004
|
-
typeof copilotModelDiscoveryService?.refresh === 'function'
|
|
9005
|
-
? copilotModelDiscoveryService.refresh('manual-refresh')
|
|
9006
|
-
: Promise.resolve({ ok: true, skipped: true, models: [], error: null }),
|
|
9007
|
-
deepseekSettings?.enabled === true && deepseekSettingsService
|
|
9008
|
-
? deepseekSettingsService.refreshModels()
|
|
9009
|
-
: Promise.resolve({ ok: true, skipped: true, models: [], error: null }),
|
|
9010
|
-
claudeApiSettings?.enabled === true && claudeApiSettingsService
|
|
9011
|
-
? claudeApiSettingsService.refreshModels()
|
|
9012
|
-
: Promise.resolve({ ok: true, skipped: true, models: [], error: null }),
|
|
9013
|
-
];
|
|
9014
|
-
const [
|
|
9015
|
-
cliRefresh, openAIRefresh, claudeRefresh, cursorRefresh, grokRefresh, copilotDiscoveryRefresh, deepseekRefresh, claudeApiRefresh,
|
|
9016
|
-
] = await Promise.allSettled(refreshTasks);
|
|
9098
|
+
refreshProviderModels({ table: providerRefreshTable, reason: 'manual-refresh' }),
|
|
9099
|
+
]);
|
|
9017
9100
|
if (cliRefresh.status === 'rejected') throw cliRefresh.reason;
|
|
9018
|
-
const
|
|
9019
|
-
|
|
9020
|
-
: {
|
|
9021
|
-
ok: false,
|
|
9022
|
-
models: Array.isArray(openAISettings?.models) ? openAISettings.models : [],
|
|
9023
|
-
error: openAIRefresh.reason?.message || 'OpenAI model discovery failed',
|
|
9024
|
-
};
|
|
9025
|
-
const claudeModelDiscovery = claudeRefresh.status === 'fulfilled'
|
|
9026
|
-
? claudeRefresh.value
|
|
9027
|
-
: {
|
|
9028
|
-
ok: false,
|
|
9029
|
-
models: Array.isArray(claudeSettings?.models) ? claudeSettings.models : [],
|
|
9030
|
-
error: claudeRefresh.reason?.message || 'Claude model discovery failed',
|
|
9031
|
-
};
|
|
9032
|
-
const cursorModelDiscovery = cursorRefresh.status === 'fulfilled'
|
|
9033
|
-
? cursorRefresh.value
|
|
9034
|
-
: {
|
|
9035
|
-
ok: false,
|
|
9036
|
-
models: Array.isArray(cursorSettings?.models) ? cursorSettings.models : [],
|
|
9037
|
-
error: cursorRefresh.reason?.message || 'Cursor model discovery failed',
|
|
9038
|
-
};
|
|
9039
|
-
const grokModelDiscovery = grokRefresh.status === 'fulfilled'
|
|
9040
|
-
? grokRefresh.value
|
|
9041
|
-
: {
|
|
9042
|
-
ok: false,
|
|
9043
|
-
models: Array.isArray(grokSettings?.models) ? grokSettings.models : [],
|
|
9044
|
-
error: grokRefresh.reason?.message || 'Grok model discovery failed',
|
|
9045
|
-
};
|
|
9046
|
-
const deepseekModelDiscovery = deepseekRefresh.status === 'fulfilled'
|
|
9047
|
-
? deepseekRefresh.value
|
|
9048
|
-
: {
|
|
9049
|
-
ok: false,
|
|
9050
|
-
models: Array.isArray(deepseekSettings?.models) ? deepseekSettings.models : [],
|
|
9051
|
-
error: deepseekRefresh.reason?.message || 'DeepSeek model discovery failed',
|
|
9052
|
-
};
|
|
9053
|
-
const claudeApiModelDiscovery = claudeApiRefresh.status === 'fulfilled'
|
|
9054
|
-
? claudeApiRefresh.value
|
|
9055
|
-
: {
|
|
9056
|
-
ok: false,
|
|
9057
|
-
models: Array.isArray(claudeApiSettings?.models) ? claudeApiSettings.models : [],
|
|
9058
|
-
error: claudeApiRefresh.reason?.message || 'Claude API model discovery failed',
|
|
9059
|
-
};
|
|
9060
|
-
const copilotModelDiscovery = copilotDiscoveryRefresh.status === 'fulfilled'
|
|
9061
|
-
? copilotDiscoveryRefresh.value
|
|
9062
|
-
: { ok: false, models: [], error: copilotDiscoveryRefresh.reason?.message || 'Copilot model discovery failed' };
|
|
9063
|
-
io.emit('models_updated', buildModelCatalogWithProviders(
|
|
9101
|
+
const results = providerRefresh.status === 'fulfilled' ? providerRefresh.value.results : {};
|
|
9102
|
+
io.emit('models_updated', modelsUpdatedPayload(
|
|
9064
9103
|
getModelCatalogState(),
|
|
9065
9104
|
));
|
|
9066
9105
|
return res.json({
|
|
9067
9106
|
ok: true,
|
|
9068
9107
|
...buildModelVariantCatalogPayloadForRoute(),
|
|
9069
|
-
openAIModelDiscovery,
|
|
9070
|
-
claudeModelDiscovery,
|
|
9071
|
-
cursorModelDiscovery,
|
|
9072
|
-
grokModelDiscovery,
|
|
9073
|
-
deepseekModelDiscovery,
|
|
9074
|
-
claudeApiModelDiscovery,
|
|
9075
|
-
copilotModelDiscovery,
|
|
9108
|
+
openAIModelDiscovery: results.openai,
|
|
9109
|
+
claudeModelDiscovery: results.claude,
|
|
9110
|
+
cursorModelDiscovery: results.cursor,
|
|
9111
|
+
grokModelDiscovery: results.grok,
|
|
9112
|
+
deepseekModelDiscovery: results.deepseek,
|
|
9113
|
+
claudeApiModelDiscovery: results['claude-api'],
|
|
9114
|
+
copilotModelDiscovery: results.github,
|
|
9076
9115
|
});
|
|
9077
9116
|
} catch (error) {
|
|
9078
9117
|
return res.status(500).json({
|
|
@@ -9087,11 +9126,28 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
9087
9126
|
if (typeof setEnabledModelVariants !== 'function') {
|
|
9088
9127
|
return res.status(501).json({ error: 'Model variant updates are unavailable' });
|
|
9089
9128
|
}
|
|
9090
|
-
const
|
|
9129
|
+
const hasVariantIds = Array.isArray(req.body?.enabledVariantIds);
|
|
9130
|
+
const autoEnableNew = req.body?.autoEnableNew;
|
|
9131
|
+
if (autoEnableNew !== undefined && typeof autoEnableNew !== 'boolean') {
|
|
9132
|
+
return res.status(400).json({ error: 'autoEnableNew must be true or false' });
|
|
9133
|
+
}
|
|
9134
|
+
if (!hasVariantIds && autoEnableNew === undefined) {
|
|
9135
|
+
return res.status(400).json({ error: 'No model variant update provided' });
|
|
9136
|
+
}
|
|
9137
|
+
const enabledVariantIds = hasVariantIds
|
|
9091
9138
|
? req.body.enabledVariantIds.map((value) => String(value || '').trim()).filter(Boolean)
|
|
9092
9139
|
: [];
|
|
9093
|
-
|
|
9094
|
-
|
|
9140
|
+
// Every row off would leave the pickers with the fallback model only.
|
|
9141
|
+
if (hasVariantIds && !enabledVariantIds.length) {
|
|
9142
|
+
return res.status(400).json({ error: 'Select at least one model variant' });
|
|
9143
|
+
}
|
|
9144
|
+
// The rows the browser drew: only those change (an older page sends none).
|
|
9145
|
+
const shownVariantIds = Array.isArray(req.body?.shownVariantIds)
|
|
9146
|
+
? req.body.shownVariantIds.map((value) => String(value || '').trim()).filter(Boolean)
|
|
9147
|
+
: null;
|
|
9148
|
+
if (hasVariantIds) setEnabledModelVariants(enabledVariantIds, shownVariantIds ? { shownVariantIds } : undefined);
|
|
9149
|
+
if (typeof autoEnableNew === 'boolean') setCopilotAutoEnableNew?.(autoEnableNew);
|
|
9150
|
+
io.emit('models_updated', modelsUpdatedPayload(
|
|
9095
9151
|
getModelCatalogState(),
|
|
9096
9152
|
));
|
|
9097
9153
|
return res.json({
|
|
@@ -9100,6 +9156,74 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
9100
9156
|
});
|
|
9101
9157
|
});
|
|
9102
9158
|
|
|
9159
|
+
// Select Models was opened: every provider's "New" flags clear.
|
|
9160
|
+
app.post('/api/model-variants/seen', auth, (req, res) => {
|
|
9161
|
+
ensureSessionId(req, res);
|
|
9162
|
+
const markers = [
|
|
9163
|
+
() => markCopilotModelsSeen(),
|
|
9164
|
+
() => markProviderModelsSeen('openai'),
|
|
9165
|
+
() => markProviderModelsSeen('claude'),
|
|
9166
|
+
() => markProviderModelsSeen('cursor'),
|
|
9167
|
+
() => markProviderModelsSeen('grok'),
|
|
9168
|
+
() => deepseekSettingsService?.markModelsSeen?.() === true,
|
|
9169
|
+
() => claudeApiSettingsService?.markModelsSeen?.() === true,
|
|
9170
|
+
];
|
|
9171
|
+
let changed = false;
|
|
9172
|
+
for (const mark of markers) {
|
|
9173
|
+
try {
|
|
9174
|
+
if (mark() === true) changed = true;
|
|
9175
|
+
} catch {
|
|
9176
|
+
/* one provider's map does not hold up the others */
|
|
9177
|
+
}
|
|
9178
|
+
}
|
|
9179
|
+
if (changed) {
|
|
9180
|
+
io.emit('models_updated', modelsUpdatedPayload(
|
|
9181
|
+
getModelCatalogState(),
|
|
9182
|
+
));
|
|
9183
|
+
}
|
|
9184
|
+
return res.json({ ok: true, unseenNewModelCount: countUnseenNewModels() });
|
|
9185
|
+
});
|
|
9186
|
+
|
|
9187
|
+
// "Check for new models" (Settings → General → Models): the switch, the
|
|
9188
|
+
// interval, the providers left out, and each enabled provider's last check.
|
|
9189
|
+
function modelRefreshSettingsPayload(settings) {
|
|
9190
|
+
const last = modelRefreshLedger?.readLast?.() || {};
|
|
9191
|
+
const lastOf = (id) => (last[id] ? { at: last[id].at, ok: last[id].ok, error: last[id].error } : null);
|
|
9192
|
+
const ask = (id, gate) => {
|
|
9193
|
+
try {
|
|
9194
|
+
return providerRefreshTable[id]?.[gate]?.() === true;
|
|
9195
|
+
} catch {
|
|
9196
|
+
return false;
|
|
9197
|
+
}
|
|
9198
|
+
};
|
|
9199
|
+
// Every switched-on provider; one that is not set up is not checked and
|
|
9200
|
+
// says so.
|
|
9201
|
+
const providers = MODEL_REFRESH_PROVIDERS
|
|
9202
|
+
.filter(({ id }) => ask(id, 'isEnabled'))
|
|
9203
|
+
.map(({ id, label }) => ({ id, label, setUp: ask(id, 'isSetUp'), last: lastOf(id) }));
|
|
9204
|
+
return {
|
|
9205
|
+
auto: settings.auto,
|
|
9206
|
+
intervalHours: settings.intervalHours,
|
|
9207
|
+
intervalChoices: [...MODEL_REFRESH_INTERVAL_HOURS_CHOICES],
|
|
9208
|
+
default: { auto: DEFAULT_MODEL_REFRESH_AUTO, intervalHours: DEFAULT_MODEL_REFRESH_INTERVAL_HOURS },
|
|
9209
|
+
excluded: settings.excluded,
|
|
9210
|
+
providers,
|
|
9211
|
+
last: Object.fromEntries(Object.keys(last).map((id) => [id, { at: last[id].at, ok: last[id].ok }])),
|
|
9212
|
+
};
|
|
9213
|
+
}
|
|
9214
|
+
|
|
9215
|
+
app.get('/api/model-refresh-settings', auth, (_req, res) => {
|
|
9216
|
+
if (!modelRefreshSettings) return res.status(501).json({ error: 'Model refresh settings are unavailable' });
|
|
9217
|
+
return res.json(modelRefreshSettingsPayload(modelRefreshSettings.get()));
|
|
9218
|
+
});
|
|
9219
|
+
|
|
9220
|
+
app.post('/api/model-refresh-settings', auth, (req, res) => {
|
|
9221
|
+
if (!modelRefreshSettings) return res.status(501).json({ error: 'Model refresh settings are unavailable' });
|
|
9222
|
+
const result = modelRefreshSettings.update(req.body);
|
|
9223
|
+
if (!result.ok) return res.status(400).json({ error: result.error });
|
|
9224
|
+
return res.json(modelRefreshSettingsPayload(result.settings));
|
|
9225
|
+
});
|
|
9226
|
+
|
|
9103
9227
|
app.get('/api/models', auth, (req, res) => {
|
|
9104
9228
|
ensureSessionId(req, res);
|
|
9105
9229
|
const modelState = buildModelCatalogWithProviders(
|
|
@@ -9121,6 +9245,8 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
9121
9245
|
claudeContextTiersByModel: modelState.claudeContextTiersByModel || {},
|
|
9122
9246
|
claudeApiContextTiersByModel: modelState.claudeApiContextTiersByModel || {},
|
|
9123
9247
|
providersByModel: modelState.providersByModel || {},
|
|
9248
|
+
// Models flagged new in Select Models: the menu item's dot.
|
|
9249
|
+
unseenNewModelCount: countUnseenNewModels(),
|
|
9124
9250
|
stale: modelState.stale,
|
|
9125
9251
|
metadataValid: modelState.metadataValid === true,
|
|
9126
9252
|
reasoningMetadataValid: modelState.reasoningMetadataValid === true,
|
|
@@ -9142,7 +9268,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
9142
9268
|
source: source || 'relay-extension',
|
|
9143
9269
|
error,
|
|
9144
9270
|
});
|
|
9145
|
-
io.emit('models_updated',
|
|
9271
|
+
io.emit('models_updated', modelsUpdatedPayload(
|
|
9146
9272
|
getModelCatalogState(),
|
|
9147
9273
|
));
|
|
9148
9274
|
res.json({
|