@oar-sh/oar 0.9.16 → 0.9.17
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/.github/extensions/web-relay/polling/polling-loop.mjs +14 -87
- package/CHANGELOG.md +220 -0
- package/README.md +12 -11
- package/package.json +2 -1
- package/server/claude-worker/claude-attachments.mjs +7 -4
- package/server/claude-worker/claude-sdk-adapter.mjs +167 -9
- package/server/claude-worker/claude-session-process.mjs +127 -29
- package/server/claude-worker/claude-session-worker-main.mjs +266 -0
- package/server/claude-worker/claude-session-worker.mjs +2 -253
- package/server/claude-worker/claude-turn-failure.mjs +8 -0
- package/server/claude-worker/claude-turn-publisher.mjs +282 -13
- package/server/claude-worker/claude-usage-totals.mjs +108 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +55 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +18 -4
- package/server/copilot-worker/copilot-sdk-session-process.mjs +71 -5
- package/server/db-schema.mjs +63 -0
- package/server/deepseek-worker/deepseek-claude-backend.mjs +433 -0
- package/server/deepseek-worker/deepseek-session-worker.mjs +37 -0
- package/server/migrations/0024-deepseek-usage-totals.mjs +151 -0
- package/server/migrations/0025-deepseek-usage-requests-daily.mjs +179 -0
- package/server/migrations/0026-deepseek-usage-served-models.mjs +222 -0
- package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +101 -0
- package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +99 -0
- package/server/migrations/0029-deepseek-balance-rises.mjs +99 -0
- package/server/migrations/0030-deepseek-usage-actuals.mjs +123 -0
- package/server/public/app/api-client.js +53 -0
- package/server/public/app/bootstrap.js +101 -13
- package/server/public/app/context-tier-options.mjs +4 -0
- package/server/public/app/conversation-provider-indicator.mjs +7 -0
- package/server/public/app/conversation-view.js +2 -0
- package/server/public/app/deepseek-settings-ui.js +342 -0
- package/server/public/app/inbox-ui.js +14 -3
- package/server/public/app/journal-view.js +31 -3
- package/server/public/app/model-selector-options.mjs +11 -0
- package/server/public/app/model-vision-support.mjs +2 -0
- package/server/public/app/no-conversation-state.mjs +1 -1
- package/server/public/app/plan-usage-view.mjs +165 -13
- package/server/public/app/provider-readiness.mjs +4 -1
- package/server/public/app/relay-error-ctas.mjs +8 -0
- package/server/public/app/settings-modal.js +11 -0
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +10 -0
- package/server/public/index.html +408 -33
- package/server/repositories/conversation-group-repository.mjs +13 -6
- package/server/routes/conversation-group-routes.mjs +2 -1
- package/server/routes/deepseek-usage-import-routes.mjs +72 -0
- package/server/routes/messages-routes.mjs +192 -15
- package/server/routes/sessions-routes.mjs +364 -55
- package/server/server-runtime.mjs +123 -10
- package/server/services/about-service.mjs +1 -0
- package/server/services/claude-session-leftovers.mjs +226 -0
- package/server/services/deepseek-balance-service.mjs +102 -0
- package/server/services/deepseek-settings-service.mjs +380 -0
- package/server/services/deepseek-usage-import.mjs +471 -0
- package/server/services/model-variant-catalog-service.mjs +1 -0
- package/server/services/plan-usage-contract.mjs +100 -5
- package/server/services/plan-usage-deepseek.mjs +931 -0
- package/server/services/plan-usage-service.mjs +431 -0
- package/server/services/provider-model-selection.mjs +1 -0
- package/server/services/provider-reasoning-effort.mjs +15 -2
- package/server/services/remote-relay-dispatcher.mjs +2 -1
- package/server/services/session-worker-launch-service.mjs +233 -82
- package/server/services/worker-secret-redaction.mjs +318 -0
- package/shared/context-window-fallbacks.mjs +3 -0
- package/shared/deepseek-api.mjs +137 -0
- package/shared/deepseek-models.mjs +114 -0
- package/shared/deepseek-pricing.mjs +146 -0
- package/shared/host-claude-session-env.mjs +101 -0
- package/shared/model-id.mjs +7 -0
- package/shared/preview-tool-core.mjs +1 -1
- package/shared/provider-routing.mjs +2 -2
- package/shared/remote-relay-contract.mjs +1 -1
- package/shared/steer-settle-failure.mjs +1 -0
- package/shared/worker-runtime/direct-openai-image.mjs +128 -0
- package/shared/worker-runtime/worker-log-file.mjs +60 -0
|
@@ -46,6 +46,14 @@ import { stopSessionWorkerProcesses } from '../services/session-worker-stop-serv
|
|
|
46
46
|
import { createClaudeAuthService } from '../services/claude-auth-service.mjs';
|
|
47
47
|
import { createCliInstallService } from '../services/cli-install-service.mjs';
|
|
48
48
|
import { createGrokAuthService } from '../services/grok-auth-service.mjs';
|
|
49
|
+
import { parseDeepSeekSettingsUpdateRequest } from '../services/deepseek-settings-service.mjs';
|
|
50
|
+
import {
|
|
51
|
+
DEEPSEEK_PROVIDER_TYPE,
|
|
52
|
+
DEEPSEEK_REASONING_EFFORTS,
|
|
53
|
+
DEFAULT_DEEPSEEK_MODEL,
|
|
54
|
+
DEFAULT_DEEPSEEK_REASONING_EFFORT,
|
|
55
|
+
deepseekReasoningEffortFor,
|
|
56
|
+
} from '../../shared/deepseek-models.mjs';
|
|
49
57
|
import {
|
|
50
58
|
isWithinAllowedPrefix,
|
|
51
59
|
readWorkspaceRootPathFromBody,
|
|
@@ -133,6 +141,10 @@ import {
|
|
|
133
141
|
derivePwaShortName,
|
|
134
142
|
} from '../../shared/pwa-app-name.mjs';
|
|
135
143
|
import { parseRestartResumeUpdate } from '../services/restart-resume-service.mjs';
|
|
144
|
+
import { removeClaudeSessionLeftovers } from '../services/claude-session-leftovers.mjs';
|
|
145
|
+
import { deepseekStartingBalanceNote } from '../services/plan-usage-deepseek.mjs';
|
|
146
|
+
import { DEEPSEEK_USAGE_CHECKPOINTS_TABLE } from '../migrations/0024-deepseek-usage-totals.mjs';
|
|
147
|
+
import { claudeProjectDirSlug } from '../services/claude-session-root-service.mjs';
|
|
136
148
|
|
|
137
149
|
export { mapUsageSnapshotRow };
|
|
138
150
|
|
|
@@ -794,6 +806,55 @@ export function buildModelCatalogWithGrokProvider(modelState = {}, grokSettings
|
|
|
794
806
|
};
|
|
795
807
|
}
|
|
796
808
|
|
|
809
|
+
export function buildModelCatalogWithDeepSeekProvider(modelState = {}, deepseekSettings = {}) {
|
|
810
|
+
const configured = deepseekSettings?.enabled === true;
|
|
811
|
+
const model = String(deepseekSettings?.model || '').trim();
|
|
812
|
+
if (!configured || !model) return { ...modelState };
|
|
813
|
+
const deepseekModels = [];
|
|
814
|
+
const seenDeepSeekModels = new Set();
|
|
815
|
+
const listed = Array.isArray(deepseekSettings?.models) ? deepseekSettings.models : [];
|
|
816
|
+
for (const candidate of [model, ...listed]) {
|
|
817
|
+
const modelId = String(candidate || '').trim();
|
|
818
|
+
const lowerId = modelId.toLowerCase();
|
|
819
|
+
if (!modelId || seenDeepSeekModels.has(lowerId)) continue;
|
|
820
|
+
seenDeepSeekModels.add(lowerId);
|
|
821
|
+
deepseekModels.push(modelId);
|
|
822
|
+
}
|
|
823
|
+
const models = Array.from(new Set([...(Array.isArray(modelState?.models) ? modelState.models : []), ...deepseekModels]));
|
|
824
|
+
const reasoningByModel = { ...(modelState?.reasoningByModel || {}) };
|
|
825
|
+
const deepseekReasoningByModel = {};
|
|
826
|
+
const modelMetadataByModel = { ...(modelState?.modelMetadataByModel || {}) };
|
|
827
|
+
const providersByModel = { ...(modelState?.providersByModel || {}) };
|
|
828
|
+
// DeepSeek's ids are its own ("deepseek-*"): no other provider lists them,
|
|
829
|
+
// so the provider tag is never shared with Copilot.
|
|
830
|
+
for (const deepseekModel of deepseekModels) {
|
|
831
|
+
const lowerKey = deepseekModel.toLowerCase();
|
|
832
|
+
deepseekReasoningByModel[lowerKey] = [...DEEPSEEK_REASONING_EFFORTS];
|
|
833
|
+
if (!Array.isArray(reasoningByModel[lowerKey]) || reasoningByModel[lowerKey].length === 0) {
|
|
834
|
+
reasoningByModel[lowerKey] = [...DEEPSEEK_REASONING_EFFORTS];
|
|
835
|
+
}
|
|
836
|
+
const existingProviders = Array.isArray(providersByModel[deepseekModel])
|
|
837
|
+
? providersByModel[deepseekModel]
|
|
838
|
+
: (Array.isArray(providersByModel[lowerKey]) ? providersByModel[lowerKey] : []);
|
|
839
|
+
providersByModel[deepseekModel] = Array.from(new Set([...existingProviders, DEEPSEEK_PROVIDER_TYPE]));
|
|
840
|
+
modelMetadataByModel[deepseekModel] = {
|
|
841
|
+
...(modelMetadataByModel[deepseekModel] || {}),
|
|
842
|
+
provider: modelMetadataByModel[deepseekModel]?.provider || DEEPSEEK_PROVIDER_TYPE,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
return {
|
|
846
|
+
...modelState,
|
|
847
|
+
models,
|
|
848
|
+
reasoningByModel,
|
|
849
|
+
reasoningByProvider: {
|
|
850
|
+
...(modelState?.reasoningByProvider || {}),
|
|
851
|
+
[DEEPSEEK_PROVIDER_TYPE]: deepseekReasoningByModel,
|
|
852
|
+
},
|
|
853
|
+
modelMetadataByModel,
|
|
854
|
+
providersByModel,
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
797
858
|
function normalizeRequestedProviderType(value = '') {
|
|
798
859
|
const normalized = String(value || '').trim().toLowerCase();
|
|
799
860
|
if (normalized === 'openai' || normalized === 'openai-byok') return 'openai';
|
|
@@ -803,6 +864,7 @@ function normalizeRequestedProviderType(value = '') {
|
|
|
803
864
|
if (normalized === 'claude-cloud') return 'claude-cloud';
|
|
804
865
|
if (normalized === 'cursor') return 'cursor';
|
|
805
866
|
if (normalized === 'grok' || normalized === 'xai' || normalized === 'xai-grok') return 'grok';
|
|
867
|
+
if (normalized === 'deepseek') return 'deepseek';
|
|
806
868
|
if (normalized === 'github' || normalized === 'github-copilot') return 'github';
|
|
807
869
|
return '';
|
|
808
870
|
}
|
|
@@ -1421,6 +1483,8 @@ export function buildConversationSessionRootPayload({
|
|
|
1421
1483
|
claudeNativeSessionId = '',
|
|
1422
1484
|
workspaceRootPath = '',
|
|
1423
1485
|
resolveClaudeSessionRoot = null,
|
|
1486
|
+
// The same Claude Code layout, in the DeepSeek workers' own config dir.
|
|
1487
|
+
resolveDeepSeekSessionRoot = null,
|
|
1424
1488
|
resolveCursorSessionRoot = null,
|
|
1425
1489
|
} = {}) {
|
|
1426
1490
|
const sid = String(sdkSessionId || '').trim() || String(conversationId || '').trim();
|
|
@@ -1443,10 +1507,12 @@ export function buildConversationSessionRootPayload({
|
|
|
1443
1507
|
// Claude sessions have no ~/.copilot/session-state entry — only the Copilot
|
|
1444
1508
|
// CLI creates those — so they resolve against the Agent SDK's own layout and
|
|
1445
1509
|
// never fall through to the branch below.
|
|
1446
|
-
|
|
1510
|
+
const claudeCodeProvider = String(providerType || '').trim().toLowerCase();
|
|
1511
|
+
if (claudeCodeProvider === 'claude' || claudeCodeProvider === 'deepseek') {
|
|
1447
1512
|
const nativeSessionId = String(claudeNativeSessionId || '').trim();
|
|
1448
|
-
|
|
1449
|
-
|
|
1513
|
+
const resolveRoot = claudeCodeProvider === 'deepseek' ? resolveDeepSeekSessionRoot : resolveClaudeSessionRoot;
|
|
1514
|
+
if (!nativeSessionId || typeof resolveRoot !== 'function') return null;
|
|
1515
|
+
const claudeRoot = resolveRoot({
|
|
1450
1516
|
claudeNativeSessionId: nativeSessionId,
|
|
1451
1517
|
workspaceRootPath: String(workspaceRootPath || '').trim(),
|
|
1452
1518
|
});
|
|
@@ -2068,6 +2134,10 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2068
2134
|
// is simply off.
|
|
2069
2135
|
claudeCloudSettingsService = null,
|
|
2070
2136
|
claudeCloudSessionService = null,
|
|
2137
|
+
// DeepSeek (provider `deepseek`): its key, models, base URL and
|
|
2138
|
+
// attribution (services/deepseek-settings-service.mjs). Absent in older
|
|
2139
|
+
// wiring and most tests, where the provider is simply off.
|
|
2140
|
+
deepseekSettingsService = null,
|
|
2071
2141
|
reconcileUnstartedConversationProviders = async () => ({
|
|
2072
2142
|
updatedUnstartedConversations: 0,
|
|
2073
2143
|
skippedStartedConversations: 0,
|
|
@@ -2108,6 +2178,13 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2108
2178
|
// nothing while Claude Cloud is off, and absent (tests, older wiring) the
|
|
2109
2179
|
// Claude card stays the last turn's snapshot.
|
|
2110
2180
|
claudeAccountUsageService = null,
|
|
2181
|
+
// Optional DeepSeek balance (deepseek-balance-service.mjs); it reads
|
|
2182
|
+
// nothing while DeepSeek is off, and absent (tests, older wiring) the
|
|
2183
|
+
// DeepSeek card shows the relay's estimated spend alone.
|
|
2184
|
+
deepseekBalanceService = null,
|
|
2185
|
+
// Optional DeepSeek usage export (deepseek-usage-import.mjs): its figures
|
|
2186
|
+
// replace the estimate on the days it covers; absent, the card is the estimate.
|
|
2187
|
+
deepseekUsageImportService = null,
|
|
2111
2188
|
getCursorPlanAllowanceSettings = () => ({ cursorModelsUsd: null, otherModelsUsd: null, resetDay: 1 }),
|
|
2112
2189
|
setCursorPlanAllowanceSettings = () => ({ ok: false, error: 'Cursor allowance settings are unavailable' }),
|
|
2113
2190
|
getGrokPlanAllowanceSettings = () => ({ monthlyUsd: null, resetDay: 1 }),
|
|
@@ -2139,6 +2216,10 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2139
2216
|
sessionWorkerStopOverrides = null,
|
|
2140
2217
|
resolveSessionStateRoot,
|
|
2141
2218
|
resolveClaudeSessionRoot = null,
|
|
2219
|
+
resolveDeepSeekSessionRoot = null,
|
|
2220
|
+
// DeepSeek's Claude Code config directory, which the relay owns: a deleted
|
|
2221
|
+
// conversation's leftovers there go with it (claude-session-leftovers.mjs).
|
|
2222
|
+
deepseekClaudeConfigDir = '',
|
|
2142
2223
|
resolveCursorSessionRoot = null,
|
|
2143
2224
|
getTurnCeilingMinutes = () => DEFAULT_TURN_CEILING_MINUTES,
|
|
2144
2225
|
setTurnCeilingMinutes = () => ({ ok: false, error: 'Turn ceiling settings are unavailable' }),
|
|
@@ -2356,6 +2437,9 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2356
2437
|
FROM message_usage_snapshots
|
|
2357
2438
|
WHERE conversation_id = ?
|
|
2358
2439
|
`);
|
|
2440
|
+
// Asked on each delete: the usage service creates the table at its own
|
|
2441
|
+
// start, which may come after these routes'.
|
|
2442
|
+
const findDeepSeekUsageCheckpointsTable = db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`);
|
|
2359
2443
|
const hardDeleteConversationRows = db.transaction((conversationId) => {
|
|
2360
2444
|
if (typeof stmts.deleteConvQuestions?.run === 'function') {
|
|
2361
2445
|
stmts.deleteConvQuestions.run(conversationId);
|
|
@@ -2385,6 +2469,11 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2385
2469
|
db.prepare(`DELETE FROM queue WHERE conversation_id = ?`).run(conversationId);
|
|
2386
2470
|
db.prepare(`DELETE FROM messages WHERE conversation_id = ?`).run(conversationId);
|
|
2387
2471
|
db.prepare(`DELETE FROM runtime_sessions WHERE conversation_id = ?`).run(conversationId);
|
|
2472
|
+
// A DeepSeek conversation's usage checkpoints are only good for diffing its
|
|
2473
|
+
// later reports; the totals and days they fed stay, as history.
|
|
2474
|
+
if (findDeepSeekUsageCheckpointsTable.get(DEEPSEEK_USAGE_CHECKPOINTS_TABLE)) {
|
|
2475
|
+
db.prepare(`DELETE FROM ${DEEPSEEK_USAGE_CHECKPOINTS_TABLE} WHERE conversation_id = ?`).run(conversationId);
|
|
2476
|
+
}
|
|
2388
2477
|
// The share access log goes with it; the share rows follow the foreign key.
|
|
2389
2478
|
shares().forgetConversation(conversationId);
|
|
2390
2479
|
db.prepare(`DELETE FROM conversations WHERE id = ?`).run(conversationId);
|
|
@@ -2410,17 +2499,25 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2410
2499
|
const sessionLaunchMutex = createKeyedMutex();
|
|
2411
2500
|
const relaunchCoalescer = createRelaunchCoalescer({ windowMs: RELAUNCH_COALESCE_WINDOW_MS });
|
|
2412
2501
|
|
|
2502
|
+
function getDeepSeekProviderSettings() {
|
|
2503
|
+
return deepseekSettingsService?.getSettings?.()
|
|
2504
|
+
|| { configured: false, enabled: false, model: DEFAULT_DEEPSEEK_MODEL, models: [], availableModels: [] };
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2413
2507
|
// Layer every enabled provider's models onto the base Copilot catalog.
|
|
2414
2508
|
function buildModelCatalogWithProviders(modelState, openAISettings = getOpenAIProviderSettings()) {
|
|
2415
|
-
return
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2509
|
+
return buildModelCatalogWithDeepSeekProvider(
|
|
2510
|
+
buildModelCatalogWithGrokProvider(
|
|
2511
|
+
buildModelCatalogWithCursorProvider(
|
|
2512
|
+
buildModelCatalogWithClaudeProvider(
|
|
2513
|
+
buildModelCatalogWithOpenAIProvider(modelState, openAISettings),
|
|
2514
|
+
getClaudeProviderSettings(),
|
|
2515
|
+
),
|
|
2516
|
+
getCursorProviderSettings(),
|
|
2420
2517
|
),
|
|
2421
|
-
|
|
2518
|
+
getGrokProviderSettings(),
|
|
2422
2519
|
),
|
|
2423
|
-
|
|
2520
|
+
getDeepSeekProviderSettings(),
|
|
2424
2521
|
);
|
|
2425
2522
|
}
|
|
2426
2523
|
|
|
@@ -2536,10 +2633,10 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2536
2633
|
* it. Nothing else in the project folder is touched — it holds every other
|
|
2537
2634
|
* session of that workspace and the project memory.
|
|
2538
2635
|
*/
|
|
2539
|
-
function deleteClaudeCliSession({ conversation, runtimeSession }) {
|
|
2636
|
+
function deleteClaudeCliSession({ conversation, runtimeSession, resolveRoot = resolveClaudeSessionRoot }) {
|
|
2540
2637
|
const nativeSessionId = String(runtimeSession?.claude_native_session_id || '').trim();
|
|
2541
|
-
if (!nativeSessionId || typeof
|
|
2542
|
-
const anchors =
|
|
2638
|
+
if (!nativeSessionId || typeof resolveRoot !== 'function') return { deleted: null };
|
|
2639
|
+
const anchors = resolveRoot({
|
|
2543
2640
|
claudeNativeSessionId: nativeSessionId,
|
|
2544
2641
|
workspaceRootPath: String(
|
|
2545
2642
|
conversation?.runtime_workspace_root_path || conversation?.configured_workspace_root_path || '',
|
|
@@ -2553,7 +2650,25 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2553
2650
|
.filter((target) => path.dirname(target) === projectDir
|
|
2554
2651
|
&& [nativeSessionId, `${nativeSessionId}.jsonl`].includes(path.basename(target)));
|
|
2555
2652
|
for (const target of targets) fs.rmSync(target, { recursive: true, force: true });
|
|
2556
|
-
return { deleted: targets.length > 0 };
|
|
2653
|
+
return { deleted: targets.length > 0, projectDirPath: projectDir };
|
|
2654
|
+
}
|
|
2655
|
+
|
|
2656
|
+
/**
|
|
2657
|
+
* A DeepSeek conversation's Claude Code session: the transcript as for
|
|
2658
|
+
* Claude, then what the killed CLI left in the relay's own config directory
|
|
2659
|
+
* (its hook environment, registration, shell snapshot, and the project
|
|
2660
|
+
* folder once no session is left in it).
|
|
2661
|
+
*/
|
|
2662
|
+
function deleteDeepSeekCliSession({ conversation, runtimeSession }) {
|
|
2663
|
+
const outcome = deleteClaudeCliSession({ conversation, runtimeSession, resolveRoot: resolveDeepSeekSessionRoot });
|
|
2664
|
+
const configRoot = String(deepseekClaudeConfigDir || '').trim();
|
|
2665
|
+
const nativeSessionId = String(runtimeSession?.claude_native_session_id || '').trim();
|
|
2666
|
+
if (!configRoot || !nativeSessionId) return { deleted: outcome.deleted };
|
|
2667
|
+
const workspaceRoot = String(conversation?.runtime_workspace_root_path || conversation?.configured_workspace_root_path || '').trim();
|
|
2668
|
+
const projectDirPath = outcome.projectDirPath
|
|
2669
|
+
|| (workspaceRoot ? path.join(configRoot, 'projects', claudeProjectDirSlug(workspaceRoot)) : '');
|
|
2670
|
+
const removed = removeClaudeSessionLeftovers({ configRoot, nativeSessionId, projectDirPath });
|
|
2671
|
+
return { deleted: outcome.deleted === null && removed.length ? true : outcome.deleted };
|
|
2557
2672
|
}
|
|
2558
2673
|
|
|
2559
2674
|
/**
|
|
@@ -2594,6 +2709,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2594
2709
|
const provider = String(runtimeSession?.provider_type || '').trim().toLowerCase();
|
|
2595
2710
|
try {
|
|
2596
2711
|
if (provider === 'claude') return deleteClaudeCliSession({ conversation, runtimeSession });
|
|
2712
|
+
if (provider === 'deepseek') return deleteDeepSeekCliSession({ conversation, runtimeSession });
|
|
2597
2713
|
if (!sdkSessionId) return { deleted: null };
|
|
2598
2714
|
if (!provider || provider === 'github' || provider === 'openai') {
|
|
2599
2715
|
return await deleteCopilotCliSession({ sdkSessionId, conversationId });
|
|
@@ -3527,9 +3643,10 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
3527
3643
|
const providerType = String(runtimeSession?.provider_type || 'github').trim().toLowerCase();
|
|
3528
3644
|
// The cursor and grok workers post the same Claude-shaped usage blob, and
|
|
3529
3645
|
// so does the Claude Cloud worker (the cloud session's context and token
|
|
3530
|
-
// usage), so
|
|
3646
|
+
// usage), and so does the DeepSeek worker (the Claude one, pointed at
|
|
3647
|
+
// DeepSeek), so all five providers share the stored reader.
|
|
3531
3648
|
const usesStoredContextUsage = providerType === 'claude' || providerType === 'cursor'
|
|
3532
|
-
|| providerType === 'grok' || providerType === 'claude-cloud';
|
|
3649
|
+
|| providerType === 'grok' || providerType === 'claude-cloud' || providerType === 'deepseek';
|
|
3533
3650
|
|
|
3534
3651
|
const parsed = usesStoredContextUsage
|
|
3535
3652
|
? (() => {
|
|
@@ -3558,11 +3675,12 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
3558
3675
|
String(runtimeSession?.conversation_id || '').trim() || lookupId,
|
|
3559
3676
|
);
|
|
3560
3677
|
const autoCompactWindow = parseAutoCompactWindow(conversationRow?.auto_compact_window);
|
|
3561
|
-
// Only the local Claude worker
|
|
3562
|
-
// it reports that CLI on its
|
|
3678
|
+
// Only the local Claude worker (and the DeepSeek one, the same runner)
|
|
3679
|
+
// runs a CLI whose window the slider sets; it reports that CLI on its
|
|
3680
|
+
// heartbeat's steering snapshot.
|
|
3563
3681
|
const windowStatus = resolveAutoCompactWindowStatus({
|
|
3564
3682
|
storedWindow: autoCompactWindow,
|
|
3565
|
-
steering: providerType === 'claude' && copilotSessionId
|
|
3683
|
+
steering: (providerType === 'claude' || providerType === 'deepseek') && copilotSessionId
|
|
3566
3684
|
? sessionWorkerRegistry?.getWorker?.(copilotSessionId)?.steering || null
|
|
3567
3685
|
: null,
|
|
3568
3686
|
});
|
|
@@ -3813,6 +3931,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
3813
3931
|
claudeNativeSessionId: runtimeSession?.claude_native_session_id || '',
|
|
3814
3932
|
workspaceRootPath: workspaceState?.currentWorkspaceRootPath || '',
|
|
3815
3933
|
resolveClaudeSessionRoot,
|
|
3934
|
+
resolveDeepSeekSessionRoot,
|
|
3816
3935
|
resolveCursorSessionRoot,
|
|
3817
3936
|
});
|
|
3818
3937
|
const dbMessages = stmts.getMessages.all(conversationId);
|
|
@@ -4486,6 +4605,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
4486
4605
|
claudeNativeSessionId: runtimeSession?.claude_native_session_id || '',
|
|
4487
4606
|
workspaceRootPath: workspaceState?.currentWorkspaceRootPath || '',
|
|
4488
4607
|
resolveClaudeSessionRoot,
|
|
4608
|
+
resolveDeepSeekSessionRoot,
|
|
4489
4609
|
resolveCursorSessionRoot,
|
|
4490
4610
|
});
|
|
4491
4611
|
const historyCursor = { limit, beforeMessageId, beforeTimestamp, afterMessageId, afterTimestamp, aroundMessageId };
|
|
@@ -6186,6 +6306,12 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6186
6306
|
...(Array.isArray(grokSettings?.models) ? grokSettings.models : []),
|
|
6187
6307
|
].map((value) => String(value || '').trim()).filter(Boolean);
|
|
6188
6308
|
const availableGrokModels = new Set(grokModelCatalog.map((value) => value.toLowerCase()));
|
|
6309
|
+
const deepseekSettings = getDeepSeekProviderSettings();
|
|
6310
|
+
const deepseekModelCatalog = [
|
|
6311
|
+
String(deepseekSettings?.model || '').trim(),
|
|
6312
|
+
...(Array.isArray(deepseekSettings?.models) ? deepseekSettings.models : []),
|
|
6313
|
+
].map((value) => String(value || '').trim()).filter(Boolean);
|
|
6314
|
+
const availableDeepSeekModels = new Set(deepseekModelCatalog.map((value) => value.toLowerCase()));
|
|
6189
6315
|
const useOpenAIProvider = requestedProviderType === 'openai'
|
|
6190
6316
|
|| requestedProviderType === 'openai-image'
|
|
6191
6317
|
|| (
|
|
@@ -6221,6 +6347,15 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6221
6347
|
&& availableGrokModels.has(requestedBootstrapModel.toLowerCase())
|
|
6222
6348
|
)
|
|
6223
6349
|
);
|
|
6350
|
+
const useDeepSeekProvider = !useOpenAIProvider && !useClaudeProvider && !useCursorProvider && !useGrokProvider && (
|
|
6351
|
+
requestedProviderType === 'deepseek'
|
|
6352
|
+
|| (
|
|
6353
|
+
requestedProviderType === ''
|
|
6354
|
+
&& requestedBootstrapModel
|
|
6355
|
+
&& requestedBootstrapModel.toLowerCase() !== 'auto'
|
|
6356
|
+
&& availableDeepSeekModels.has(requestedBootstrapModel.toLowerCase())
|
|
6357
|
+
)
|
|
6358
|
+
);
|
|
6224
6359
|
// Managed providers resolve the request against their own catalog only.
|
|
6225
6360
|
// Routing it through the merged Copilot catalog first (resolveBootstrapModelSelection)
|
|
6226
6361
|
// silently substituted that catalog's current model whenever the requested
|
|
@@ -6266,7 +6401,13 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6266
6401
|
configuredModel: grokSettings.model,
|
|
6267
6402
|
availableModels: grokModelCatalog,
|
|
6268
6403
|
})
|
|
6269
|
-
:
|
|
6404
|
+
: (useDeepSeekProvider
|
|
6405
|
+
? resolveProviderModelSelection({
|
|
6406
|
+
requestedModel: providerRequestedModel(deepseekModelCatalog),
|
|
6407
|
+
configuredModel: deepseekSettings.model,
|
|
6408
|
+
availableModels: deepseekModelCatalog,
|
|
6409
|
+
})
|
|
6410
|
+
: null))));
|
|
6270
6411
|
// The single rejection for "this provider does not offer that model".
|
|
6271
6412
|
// Per-provider pre-checks used to duplicate it and had drifted apart in
|
|
6272
6413
|
// wording, casing rules and whether they listed the alternatives.
|
|
@@ -6275,14 +6416,16 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6275
6416
|
? 'Claude Cloud'
|
|
6276
6417
|
: (useOpenAIProvider
|
|
6277
6418
|
? 'OpenAI'
|
|
6278
|
-
: (useClaudeProvider ? 'Claude' : (useCursorProvider ? 'Cursor' : 'Grok')));
|
|
6419
|
+
: (useClaudeProvider ? 'Claude' : (useCursorProvider ? 'Cursor' : (useGrokProvider ? 'Grok' : 'DeepSeek'))));
|
|
6279
6420
|
const providerCode = useClaudeCloudProvider
|
|
6280
6421
|
? 'CLAUDE_CLOUD_MODEL_UNAVAILABLE'
|
|
6281
6422
|
: (useOpenAIProvider
|
|
6282
6423
|
? 'OPENAI_MODEL_UNAVAILABLE'
|
|
6283
6424
|
: (useClaudeProvider
|
|
6284
6425
|
? 'CLAUDE_MODEL_UNAVAILABLE'
|
|
6285
|
-
: (useCursorProvider
|
|
6426
|
+
: (useCursorProvider
|
|
6427
|
+
? 'CURSOR_MODEL_UNAVAILABLE'
|
|
6428
|
+
: (useGrokProvider ? 'GROK_MODEL_UNAVAILABLE' : 'DEEPSEEK_MODEL_UNAVAILABLE'))));
|
|
6286
6429
|
return res.status(400).json({
|
|
6287
6430
|
ok: false,
|
|
6288
6431
|
error: `${providerLabel} model "${providerModelSelection.requestedModel}" is not available`,
|
|
@@ -6302,6 +6445,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6302
6445
|
if (
|
|
6303
6446
|
remoteRequest.remote
|
|
6304
6447
|
&& !useClaudeCloudProvider && !useOpenAIProvider && !useClaudeProvider && !useCursorProvider && !useGrokProvider
|
|
6448
|
+
&& !useDeepSeekProvider
|
|
6305
6449
|
) {
|
|
6306
6450
|
const mergedModels = Array.isArray(modelState?.models) ? modelState.models : [];
|
|
6307
6451
|
const providersByModel = modelState?.providersByModel && typeof modelState.providersByModel === 'object'
|
|
@@ -6367,6 +6511,15 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6367
6511
|
code: 'GROK_NOT_CONFIGURED',
|
|
6368
6512
|
});
|
|
6369
6513
|
}
|
|
6514
|
+
if (useDeepSeekProvider && deepseekSettings?.enabled !== true) {
|
|
6515
|
+
return res.status(400).json({
|
|
6516
|
+
ok: false,
|
|
6517
|
+
error: deepseekSettings?.configured === true
|
|
6518
|
+
? 'DeepSeek is switched off for new conversations'
|
|
6519
|
+
: 'DeepSeek API key is not configured',
|
|
6520
|
+
code: 'DEEPSEEK_NOT_CONFIGURED',
|
|
6521
|
+
});
|
|
6522
|
+
}
|
|
6370
6523
|
|
|
6371
6524
|
// The session group an agent's create_session names (an id or an exact
|
|
6372
6525
|
// name): resolved before anything is written, so an unknown one creates
|
|
@@ -6421,7 +6574,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6421
6574
|
code: 'OPENAI_IMAGE_MODEL_REQUIRED',
|
|
6422
6575
|
});
|
|
6423
6576
|
}
|
|
6424
|
-
if (!useClaudeCloudProvider && !useOpenAIProvider && !useClaudeProvider && !useCursorProvider && !useGrokProvider) {
|
|
6577
|
+
if (!useClaudeCloudProvider && !useOpenAIProvider && !useClaudeProvider && !useCursorProvider && !useGrokProvider && !useDeepSeekProvider) {
|
|
6425
6578
|
const selectedProviders = Array.isArray(modelState?.providersByModel?.[String(selectedModel || '').trim().toLowerCase()])
|
|
6426
6579
|
? modelState.providersByModel[String(selectedModel || '').trim().toLowerCase()]
|
|
6427
6580
|
: [];
|
|
@@ -6461,6 +6614,15 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6461
6614
|
code: 'GROK_PROVIDER_REQUIRED',
|
|
6462
6615
|
});
|
|
6463
6616
|
}
|
|
6617
|
+
const deepseekOnlySelection = selectedProviders.length > 0
|
|
6618
|
+
&& selectedProviders.every((provider) => String(provider || '').trim().toLowerCase() === DEEPSEEK_PROVIDER_TYPE);
|
|
6619
|
+
if (deepseekOnlySelection) {
|
|
6620
|
+
return res.status(400).json({
|
|
6621
|
+
ok: false,
|
|
6622
|
+
error: `Model "${selectedModel}" requires the DeepSeek provider`,
|
|
6623
|
+
code: 'DEEPSEEK_PROVIDER_REQUIRED',
|
|
6624
|
+
});
|
|
6625
|
+
}
|
|
6464
6626
|
}
|
|
6465
6627
|
|
|
6466
6628
|
const bootstrapProviderType = useClaudeCloudProvider
|
|
@@ -6471,7 +6633,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6471
6633
|
? 'claude'
|
|
6472
6634
|
: (useCursorProvider
|
|
6473
6635
|
? 'cursor'
|
|
6474
|
-
: (useGrokProvider ? 'grok' : 'github'))));
|
|
6636
|
+
: (useGrokProvider ? 'grok' : (useDeepSeekProvider ? 'deepseek' : 'github')))));
|
|
6475
6637
|
// Preferences are written in the same transaction as the conversation, so
|
|
6476
6638
|
// the caller's current mode has to arrive with the request: without it every
|
|
6477
6639
|
// new chat would be stored as the default one and reset the composer. An
|
|
@@ -6487,6 +6649,9 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6487
6649
|
const bootstrapReasoning = resolveProviderReasoningEffort({
|
|
6488
6650
|
requestedEffort: req.body?.reasoningEffort || copilotVariantEffort,
|
|
6489
6651
|
strict: bootstrapProviderType === 'openai',
|
|
6652
|
+
...(bootstrapProviderType === 'deepseek'
|
|
6653
|
+
? { defaultEffort: DEFAULT_DEEPSEEK_REASONING_EFFORT, aliasFor: deepseekReasoningEffortFor }
|
|
6654
|
+
: {}),
|
|
6490
6655
|
supportedEfforts: supportedReasoningEffortsForProviderModel({
|
|
6491
6656
|
providerType: bootstrapProviderType,
|
|
6492
6657
|
model: selectedModel,
|
|
@@ -6536,7 +6701,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6536
6701
|
{
|
|
6537
6702
|
assignConfiguredProvider: true,
|
|
6538
6703
|
providerType: bootstrapProviderType,
|
|
6539
|
-
providerModel: (useClaudeCloudProvider || useOpenAIProvider || useClaudeProvider || useCursorProvider || useGrokProvider) ? selectedModel : null,
|
|
6704
|
+
providerModel: (useClaudeCloudProvider || useOpenAIProvider || useClaudeProvider || useCursorProvider || useGrokProvider || useDeepSeekProvider) ? selectedModel : null,
|
|
6540
6705
|
},
|
|
6541
6706
|
);
|
|
6542
6707
|
// What the cloud session will clone, stored with the conversation so
|
|
@@ -7511,6 +7676,40 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7511
7676
|
});
|
|
7512
7677
|
});
|
|
7513
7678
|
|
|
7679
|
+
// A rotated API key only reaches a worker's environment at spawn: live
|
|
7680
|
+
// workers of the provider keep the old key and fail each turn after the
|
|
7681
|
+
// rotation. Stops them; the primer or the next delivery respawns them with
|
|
7682
|
+
// the fresh key from settings.
|
|
7683
|
+
async function stopLiveProviderWorkersForKeyRotation(providerType) {
|
|
7684
|
+
const rows = db.prepare(`
|
|
7685
|
+
SELECT DISTINCT NULLIF(sdk_session_id, '') AS sdk_session_id
|
|
7686
|
+
FROM runtime_sessions
|
|
7687
|
+
WHERE LOWER(COALESCE(provider_type, '')) = ?
|
|
7688
|
+
AND NULLIF(sdk_session_id, '') IS NOT NULL
|
|
7689
|
+
`).all(providerType);
|
|
7690
|
+
for (const row of rows) {
|
|
7691
|
+
const sid = String(row.sdk_session_id || '').trim();
|
|
7692
|
+
if (!sid) continue;
|
|
7693
|
+
const workerState = sessionWorkerSupervisor?.getWorkerState?.(sid) || null;
|
|
7694
|
+
if (!workerState) continue;
|
|
7695
|
+
try {
|
|
7696
|
+
const stopped = await stopIdleWorkspaceRootSession({
|
|
7697
|
+
sdkSessionId: sid,
|
|
7698
|
+
worker: sessionWorkerRegistry?.getWorker?.(sid) || null,
|
|
7699
|
+
sessionWorkerSupervisor,
|
|
7700
|
+
sessionWorkerRegistry,
|
|
7701
|
+
sessionWorkerProcessInspector,
|
|
7702
|
+
});
|
|
7703
|
+
if (stopped.ok) {
|
|
7704
|
+
sessionWorkerSupervisor?.clearRestartSchedule?.(sid, { resetKilledMarker: true });
|
|
7705
|
+
console.log(`[sessions] ${providerType} worker ${sid.slice(0, 8)} stopped for API key rotation`);
|
|
7706
|
+
}
|
|
7707
|
+
} catch (error) {
|
|
7708
|
+
console.warn(`[sessions] ${providerType} worker ${sid.slice(0, 8)} stop failed on key rotation: ${error?.message || error}`);
|
|
7709
|
+
}
|
|
7710
|
+
}
|
|
7711
|
+
}
|
|
7712
|
+
|
|
7514
7713
|
app.get('/api/settings/cursor', auth, (_req, res) => {
|
|
7515
7714
|
const settings = getCursorProviderSettings();
|
|
7516
7715
|
return res.json({
|
|
@@ -7542,33 +7741,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7542
7741
|
// fail each turn after the rotation. Stop them now; the primer or the
|
|
7543
7742
|
// next delivery respawns them with the fresh key from settings.
|
|
7544
7743
|
if (parsed.apiKey && previous?.configured === true) {
|
|
7545
|
-
|
|
7546
|
-
SELECT DISTINCT NULLIF(sdk_session_id, '') AS sdk_session_id
|
|
7547
|
-
FROM runtime_sessions
|
|
7548
|
-
WHERE LOWER(COALESCE(provider_type, '')) = 'cursor'
|
|
7549
|
-
AND NULLIF(sdk_session_id, '') IS NOT NULL
|
|
7550
|
-
`).all();
|
|
7551
|
-
for (const row of cursorSessionRows) {
|
|
7552
|
-
const sid = String(row.sdk_session_id || '').trim();
|
|
7553
|
-
if (!sid) continue;
|
|
7554
|
-
const workerState = sessionWorkerSupervisor?.getWorkerState?.(sid) || null;
|
|
7555
|
-
if (!workerState) continue;
|
|
7556
|
-
try {
|
|
7557
|
-
const stopped = await stopIdleWorkspaceRootSession({
|
|
7558
|
-
sdkSessionId: sid,
|
|
7559
|
-
worker: sessionWorkerRegistry?.getWorker?.(sid) || null,
|
|
7560
|
-
sessionWorkerSupervisor,
|
|
7561
|
-
sessionWorkerRegistry,
|
|
7562
|
-
sessionWorkerProcessInspector,
|
|
7563
|
-
});
|
|
7564
|
-
if (stopped.ok) {
|
|
7565
|
-
sessionWorkerSupervisor?.clearRestartSchedule?.(sid, { resetKilledMarker: true });
|
|
7566
|
-
console.log(`[sessions] cursor worker ${sid.slice(0, 8)} stopped for API key rotation`);
|
|
7567
|
-
}
|
|
7568
|
-
} catch (error) {
|
|
7569
|
-
console.warn(`[sessions] cursor worker ${sid.slice(0, 8)} stop failed on key rotation: ${error?.message || error}`);
|
|
7570
|
-
}
|
|
7571
|
-
}
|
|
7744
|
+
await stopLiveProviderWorkersForKeyRotation('cursor');
|
|
7572
7745
|
}
|
|
7573
7746
|
const shouldDiscover = result.enabled === true && (
|
|
7574
7747
|
!!parsed.apiKey
|
|
@@ -7626,6 +7799,96 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7626
7799
|
});
|
|
7627
7800
|
});
|
|
7628
7801
|
|
|
7802
|
+
// --- DeepSeek ----------------------------------------------------------
|
|
7803
|
+
// The key is written here and never read back: GET and every answer carry
|
|
7804
|
+
// `configured` only. A rotated key reaches a worker only at spawn, so the
|
|
7805
|
+
// live DeepSeek workers are stopped; the next message starts them again.
|
|
7806
|
+
// What the tab shows: the stored settings, and whether the starting
|
|
7807
|
+
// balance still applies (a top-up since it was set replaces it), judged
|
|
7808
|
+
// on the currency the card's bar uses: the last balance read's main one.
|
|
7809
|
+
function describeDeepSeekSettings() {
|
|
7810
|
+
const described = deepseekSettingsService.describe();
|
|
7811
|
+
let references = [];
|
|
7812
|
+
try {
|
|
7813
|
+
references = planUsageService?.readDeepSeekBalanceReferences?.() || [];
|
|
7814
|
+
} catch {}
|
|
7815
|
+
let balances = null;
|
|
7816
|
+
try {
|
|
7817
|
+
balances = deepseekBalanceService?.peekBalance?.()?.balances || null;
|
|
7818
|
+
} catch {}
|
|
7819
|
+
const startingBalanceNote = deepseekStartingBalanceNote(
|
|
7820
|
+
described.startingBalance ? { amount: described.startingBalance, setAt: described.startingBalanceSetAt } : null,
|
|
7821
|
+
references,
|
|
7822
|
+
{ balances },
|
|
7823
|
+
);
|
|
7824
|
+
return { ...described, startingBalanceNote };
|
|
7825
|
+
}
|
|
7826
|
+
|
|
7827
|
+
app.get('/api/settings/deepseek', auth, (_req, res) => {
|
|
7828
|
+
if (!deepseekSettingsService) return res.status(501).json({ error: 'DeepSeek settings are unavailable' });
|
|
7829
|
+
return res.json(describeDeepSeekSettings());
|
|
7830
|
+
});
|
|
7831
|
+
|
|
7832
|
+
app.post('/api/settings/deepseek', auth, async (req, res) => {
|
|
7833
|
+
if (!deepseekSettingsService) return res.status(501).json({ error: 'DeepSeek settings are unavailable' });
|
|
7834
|
+
const parsed = parseDeepSeekSettingsUpdateRequest(req.body);
|
|
7835
|
+
if (!parsed.ok) return res.status(400).json({ error: parsed.error });
|
|
7836
|
+
// A starting balance a top-up has replaced is set anew when it is saved
|
|
7837
|
+
// again, the same amount too.
|
|
7838
|
+
const before = describeDeepSeekSettings();
|
|
7839
|
+
const startingBalanceReplaced = !!before.startingBalanceNote;
|
|
7840
|
+
const result = deepseekSettingsService.update(parsed, { startingBalanceReplaced });
|
|
7841
|
+
if (!result?.ok) {
|
|
7842
|
+
return res.status(result?.code === 'deepseek-key-removal-blocked' ? 409 : 400).json({
|
|
7843
|
+
error: result?.error || 'Failed to update DeepSeek settings',
|
|
7844
|
+
code: result?.code || null,
|
|
7845
|
+
activeConversationCount: Number(result?.activeConversationCount || 0),
|
|
7846
|
+
startedConversationCount: Number(result?.startedConversationCount || 0),
|
|
7847
|
+
activeQueueConversationCount: Number(result?.activeQueueConversationCount || 0),
|
|
7848
|
+
});
|
|
7849
|
+
}
|
|
7850
|
+
if (result.keyChanged) await stopLiveProviderWorkersForKeyRotation(DEEPSEEK_PROVIDER_TYPE);
|
|
7851
|
+
const discovery = result.discover
|
|
7852
|
+
? await deepseekSettingsService.refreshModels()
|
|
7853
|
+
: { ok: true, models: [], error: null };
|
|
7854
|
+
// A starting balance set anew: the balance is read right then and noted
|
|
7855
|
+
// as the latest read, so a later top-up is a rise from a balance read
|
|
7856
|
+
// after the setting, and one made before it is already in this read
|
|
7857
|
+
// (deepseekStartingBalanceState). A failed read changes nothing: the
|
|
7858
|
+
// setting then stays until a rise seen after a read that followed it.
|
|
7859
|
+
const setAt = result.settings?.startingBalanceSetAt || null;
|
|
7860
|
+
if (setAt && setAt !== before.startingBalanceSetAt) {
|
|
7861
|
+
try {
|
|
7862
|
+
const balance = await deepseekBalanceService?.getBalance?.({ force: true });
|
|
7863
|
+
planUsageService?.recordDeepSeekBalance?.(balance);
|
|
7864
|
+
} catch {}
|
|
7865
|
+
}
|
|
7866
|
+
// Removing the key moves unstarted DeepSeek conversations back to the
|
|
7867
|
+
// default provider, as for OpenAI and Cursor.
|
|
7868
|
+
const reconciliationResult = parsed.remove === true
|
|
7869
|
+
? await reconcileUnstartedConversationProviders({ enabled: false, model: DEFAULT_DEEPSEEK_MODEL, provider: DEEPSEEK_PROVIDER_TYPE })
|
|
7870
|
+
: { updatedUnstartedConversations: 0, skippedStartedConversations: 0, skippedActiveQueueConversations: 0, failedConversations: [] };
|
|
7871
|
+
const failedConversations = Array.isArray(reconciliationResult?.failedConversations) ? reconciliationResult.failedConversations : [];
|
|
7872
|
+
const settingsPayload = {
|
|
7873
|
+
...describeDeepSeekSettings(),
|
|
7874
|
+
reconciliation: {
|
|
7875
|
+
updatedUnstartedConversations: Number(reconciliationResult?.updatedUnstartedConversations || 0),
|
|
7876
|
+
skippedStartedConversations: Number(reconciliationResult?.skippedStartedConversations || 0),
|
|
7877
|
+
skippedActiveQueueConversations: Number(reconciliationResult?.skippedActiveQueueConversations || 0),
|
|
7878
|
+
failedConversations,
|
|
7879
|
+
},
|
|
7880
|
+
};
|
|
7881
|
+
io.emit('models_updated', buildModelCatalogWithProviders(getModelCatalogState()));
|
|
7882
|
+
io.emit('deepseek_settings_updated', settingsPayload);
|
|
7883
|
+
return res.json({
|
|
7884
|
+
ok: true,
|
|
7885
|
+
...settingsPayload,
|
|
7886
|
+
warning: failedConversations.length
|
|
7887
|
+
? `${failedConversations.length} unstarted conversation(s) could not switch provider.`
|
|
7888
|
+
: (discovery?.ok ? null : (discovery?.error || 'DeepSeek model discovery failed')),
|
|
7889
|
+
});
|
|
7890
|
+
});
|
|
7891
|
+
|
|
7629
7892
|
app.get('/api/settings/turn-ceiling', auth, (_req, res) => {
|
|
7630
7893
|
res.json({
|
|
7631
7894
|
ceilingMinutes: getTurnCeilingMinutes(),
|
|
@@ -8193,6 +8456,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8193
8456
|
const claudeSettings = getClaudeProviderSettings();
|
|
8194
8457
|
const cursorSettings = getCursorProviderSettings();
|
|
8195
8458
|
const grokSettings = getGrokProviderSettings();
|
|
8459
|
+
const deepseekSettings = getDeepSeekProviderSettings();
|
|
8196
8460
|
return {
|
|
8197
8461
|
...buildModelVariantCatalogPayload({
|
|
8198
8462
|
rows,
|
|
@@ -8222,6 +8486,13 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8222
8486
|
enabledModels: Array.isArray(grokSettings.models) ? grokSettings.models : [],
|
|
8223
8487
|
}
|
|
8224
8488
|
: null,
|
|
8489
|
+
deepseekModels: deepseekSettings?.enabled === true
|
|
8490
|
+
? {
|
|
8491
|
+
defaultModel: deepseekSettings.model,
|
|
8492
|
+
availableModels: Array.isArray(deepseekSettings.availableModels) ? deepseekSettings.availableModels : [],
|
|
8493
|
+
enabledModels: Array.isArray(deepseekSettings.models) ? deepseekSettings.models : [],
|
|
8494
|
+
}
|
|
8495
|
+
: null,
|
|
8225
8496
|
};
|
|
8226
8497
|
}
|
|
8227
8498
|
|
|
@@ -8240,6 +8511,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8240
8511
|
const claudeSettings = getClaudeProviderSettings();
|
|
8241
8512
|
const cursorSettings = getCursorProviderSettings();
|
|
8242
8513
|
const grokSettings = getGrokProviderSettings();
|
|
8514
|
+
const deepseekSettings = getDeepSeekProviderSettings();
|
|
8243
8515
|
const refreshTasks = [
|
|
8244
8516
|
refreshModelVariantCatalogFromCli(),
|
|
8245
8517
|
openAISettings?.enabled === true
|
|
@@ -8260,8 +8532,11 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8260
8532
|
typeof copilotModelDiscoveryService?.refresh === 'function'
|
|
8261
8533
|
? copilotModelDiscoveryService.refresh('manual-refresh')
|
|
8262
8534
|
: Promise.resolve({ ok: true, skipped: true, models: [], error: null }),
|
|
8535
|
+
deepseekSettings?.enabled === true && deepseekSettingsService
|
|
8536
|
+
? deepseekSettingsService.refreshModels()
|
|
8537
|
+
: Promise.resolve({ ok: true, skipped: true, models: [], error: null }),
|
|
8263
8538
|
];
|
|
8264
|
-
const [cliRefresh, openAIRefresh, claudeRefresh, cursorRefresh, grokRefresh, copilotDiscoveryRefresh] = await Promise.allSettled(refreshTasks);
|
|
8539
|
+
const [cliRefresh, openAIRefresh, claudeRefresh, cursorRefresh, grokRefresh, copilotDiscoveryRefresh, deepseekRefresh] = await Promise.allSettled(refreshTasks);
|
|
8265
8540
|
if (cliRefresh.status === 'rejected') throw cliRefresh.reason;
|
|
8266
8541
|
const openAIModelDiscovery = openAIRefresh.status === 'fulfilled'
|
|
8267
8542
|
? openAIRefresh.value
|
|
@@ -8291,6 +8566,13 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8291
8566
|
models: Array.isArray(grokSettings?.models) ? grokSettings.models : [],
|
|
8292
8567
|
error: grokRefresh.reason?.message || 'Grok model discovery failed',
|
|
8293
8568
|
};
|
|
8569
|
+
const deepseekModelDiscovery = deepseekRefresh.status === 'fulfilled'
|
|
8570
|
+
? deepseekRefresh.value
|
|
8571
|
+
: {
|
|
8572
|
+
ok: false,
|
|
8573
|
+
models: Array.isArray(deepseekSettings?.models) ? deepseekSettings.models : [],
|
|
8574
|
+
error: deepseekRefresh.reason?.message || 'DeepSeek model discovery failed',
|
|
8575
|
+
};
|
|
8294
8576
|
const copilotModelDiscovery = copilotDiscoveryRefresh.status === 'fulfilled'
|
|
8295
8577
|
? copilotDiscoveryRefresh.value
|
|
8296
8578
|
: { ok: false, models: [], error: copilotDiscoveryRefresh.reason?.message || 'Copilot model discovery failed' };
|
|
@@ -8304,6 +8586,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8304
8586
|
claudeModelDiscovery,
|
|
8305
8587
|
cursorModelDiscovery,
|
|
8306
8588
|
grokModelDiscovery,
|
|
8589
|
+
deepseekModelDiscovery,
|
|
8307
8590
|
copilotModelDiscovery,
|
|
8308
8591
|
});
|
|
8309
8592
|
} catch (error) {
|
|
@@ -8399,8 +8682,9 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8399
8682
|
copilotBilling: null, // { value, at }
|
|
8400
8683
|
grokBilling: null,
|
|
8401
8684
|
cursorBilling: null,
|
|
8685
|
+
deepseekBalance: null,
|
|
8402
8686
|
};
|
|
8403
|
-
const USAGE_PROVIDER_IDS = ['github', 'claude', 'claude-cloud', 'cursor', 'grok'];
|
|
8687
|
+
const USAGE_PROVIDER_IDS = ['github', 'claude', 'claude-cloud', 'cursor', 'grok', 'deepseek'];
|
|
8404
8688
|
|
|
8405
8689
|
function parseUsageProviders(raw) {
|
|
8406
8690
|
const text = Array.isArray(raw) ? raw.join(',') : String(raw ?? '');
|
|
@@ -8455,6 +8739,16 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8455
8739
|
const claudeSettings = getClaudeProviderSettings();
|
|
8456
8740
|
const cursorSettings = getCursorProviderSettings();
|
|
8457
8741
|
const grokSettings = getGrokProviderSettings();
|
|
8742
|
+
const deepseekSettings = getDeepSeekProviderSettings();
|
|
8743
|
+
const deepseekEnabled = deepseekSettings?.enabled === true;
|
|
8744
|
+
// An unreadable export is no reason to lose the card: the estimate stays.
|
|
8745
|
+
const readDeepSeekActuals = () => {
|
|
8746
|
+
try {
|
|
8747
|
+
return deepseekUsageImportService?.readActuals?.() ?? null;
|
|
8748
|
+
} catch {
|
|
8749
|
+
return null;
|
|
8750
|
+
}
|
|
8751
|
+
};
|
|
8458
8752
|
let claudeCloudEnabled = false;
|
|
8459
8753
|
try {
|
|
8460
8754
|
claudeCloudEnabled = claudeCloudSettingsService?.getSettings?.()?.enabled === true;
|
|
@@ -8476,7 +8770,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8476
8770
|
: Promise.resolve(null);
|
|
8477
8771
|
|
|
8478
8772
|
// All reads side by side: the modal waits for the slowest, not the sum.
|
|
8479
|
-
const [copilot, billingEntry, grokEntry, cursorEntry, claudeAccount] = await Promise.all([
|
|
8773
|
+
const [copilot, billingEntry, grokEntry, cursorEntry, deepseekEntry, claudeAccount] = await Promise.all([
|
|
8480
8774
|
readCopilotSummary(),
|
|
8481
8775
|
cachedOrLive('copilotBilling', live('github'), () => (
|
|
8482
8776
|
typeof fetchCopilotBillingUsage === 'function'
|
|
@@ -8489,6 +8783,12 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8489
8783
|
cachedOrLive('cursorBilling', live('cursor'), () => (
|
|
8490
8784
|
cursorSettings?.enabled === true && typeof fetchCursorDashboardUsage === 'function' ? fetchCursorDashboardUsage() : null
|
|
8491
8785
|
)),
|
|
8786
|
+
// The service keeps its own minute of cache and answers a failure as
|
|
8787
|
+
// `{ ok: false, … }`, so a failed read replaces the earlier answer here
|
|
8788
|
+
// (which it carries as `previous`).
|
|
8789
|
+
cachedOrLive('deepseekBalance', live('deepseek'), () => (
|
|
8790
|
+
deepseekEnabled && typeof deepseekBalanceService?.getBalance === 'function' ? deepseekBalanceService.getBalance() : null
|
|
8791
|
+
)),
|
|
8492
8792
|
claudeAccountPromise,
|
|
8493
8793
|
]);
|
|
8494
8794
|
|
|
@@ -8521,6 +8821,14 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8521
8821
|
grokConfigured: grokSettings?.enabled === true,
|
|
8522
8822
|
grokAllowances: getGrokPlanAllowanceSettings(),
|
|
8523
8823
|
grokBilling: grokEntry.value,
|
|
8824
|
+
deepseekConfigured: deepseekEnabled,
|
|
8825
|
+
deepseekBalance: deepseekEnabled ? deepseekEntry.value : null,
|
|
8826
|
+
deepseekMonthlyBudget: deepseekEnabled ? (deepseekSettings?.monthlyBudget ?? null) : null,
|
|
8827
|
+
deepseekMonthlyBudgetCurrency: deepseekEnabled ? (deepseekSettings?.monthlyBudgetCurrency ?? null) : null,
|
|
8828
|
+
deepseekStartingBalance: deepseekEnabled && deepseekSettings?.startingBalance
|
|
8829
|
+
? { amount: deepseekSettings.startingBalance, setAt: deepseekSettings.startingBalanceSetAt }
|
|
8830
|
+
: null,
|
|
8831
|
+
deepseekActuals: deepseekEnabled ? readDeepSeekActuals() : null,
|
|
8524
8832
|
});
|
|
8525
8833
|
|
|
8526
8834
|
// Legacy top-level fields stay alongside `providers` so an older cached
|
|
@@ -8535,6 +8843,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8535
8843
|
'claude-cloud': claudeAccount?.fetchedAt || null,
|
|
8536
8844
|
cursor: cursorEntry.at,
|
|
8537
8845
|
grok: grokEntry.at,
|
|
8846
|
+
deepseek: deepseekEntry.value?.fetchedAt || deepseekEntry.at,
|
|
8538
8847
|
},
|
|
8539
8848
|
});
|
|
8540
8849
|
});
|