@oar-sh/oar 0.9.16 → 0.9.18
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/.github/extensions/web-relay/skills/tool-activity.mjs +5 -4
- package/CHANGELOG.md +412 -0
- package/README.md +15 -13
- package/package.json +2 -1
- package/server/claude-api-worker/claude-api-backend.mjs +345 -0
- package/server/claude-api-worker/claude-api-session-worker.mjs +29 -0
- package/server/claude-worker/claude-attachments.mjs +7 -4
- package/server/claude-worker/claude-cli-pinned-env.mjs +150 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +3 -3
- package/server/claude-worker/claude-sdk-adapter.mjs +199 -15
- package/server/claude-worker/claude-session-process.mjs +276 -33
- package/server/claude-worker/claude-session-worker-main.mjs +269 -0
- package/server/claude-worker/claude-session-worker.mjs +2 -253
- package/server/claude-worker/claude-subscription-key-check.mjs +64 -0
- package/server/claude-worker/claude-turn-failure.mjs +8 -0
- package/server/claude-worker/claude-turn-publisher.mjs +394 -13
- package/server/claude-worker/claude-usage-totals.mjs +161 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +56 -1
- package/server/copilot-worker/copilot-remote-relay-tool.mjs +2 -2
- package/server/copilot-worker/copilot-sdk-adapter.mjs +18 -4
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +1 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +80 -14
- package/server/cursor-worker/cursor-remote-relay-tool.mjs +3 -3
- package/server/cursor-worker/cursor-turn-runner.mjs +4 -4
- package/server/cursor-worker/sdk-message-normalizer.mjs +2 -2
- package/server/db-schema.mjs +96 -1
- package/server/deepseek-worker/deepseek-claude-backend.mjs +317 -0
- package/server/deepseek-worker/deepseek-session-worker.mjs +37 -0
- package/server/grok-worker/acp-client.mjs +1 -1
- package/server/grok-worker/grok-turn-runner.mjs +4 -4
- package/server/grok-worker/sdk-message-normalizer.mjs +1 -1
- package/server/mcp/oar-mcp-server.mjs +11 -5
- 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/migrations/0031-queue-provider.mjs +143 -0
- package/server/migrations/0032-runtime-session-billing-switch.mjs +94 -0
- package/server/migrations/0033-claude-api-usage-checkpoints.mjs +138 -0
- package/server/migrations/0034-claude-api-usage-totals.mjs +112 -0
- package/server/public/app/activity-replay-state.mjs +4 -3
- package/server/public/app/agent-sessions-settings-ui.js +1 -1
- package/server/public/app/agent-sessions-settings.mjs +1 -1
- package/server/public/app/api-client.js +105 -0
- package/server/public/app/bootstrap.js +243 -29
- package/server/public/app/claude-api-settings-ui.js +362 -0
- package/server/public/app/claude-billing-toggle-ui.js +224 -0
- package/server/public/app/claude-billing-toggle.mjs +112 -0
- package/server/public/app/context-tier-options.mjs +10 -2
- package/server/public/app/context-usage-view.mjs +1 -1
- package/server/public/app/conversation-provider-indicator.mjs +18 -0
- package/server/public/app/conversation-view.js +31 -3
- package/server/public/app/deepseek-settings-ui.js +342 -0
- package/server/public/app/inbox-ui.js +14 -3
- package/server/public/app/inbox-view.mjs +1 -1
- package/server/public/app/journal-view.js +156 -11
- package/server/public/app/model-selector-options.mjs +28 -1
- package/server/public/app/model-vision-support.mjs +2 -0
- package/server/public/app/new-conversation-model-choice.mjs +21 -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 +8 -2
- package/server/public/app/relay-error-ctas.mjs +58 -0
- package/server/public/app/relay-schedules-ui.js +1 -1
- package/server/public/app/settings-modal.js +22 -0
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +28 -1
- package/server/public/app/tool-activity-emoji.mjs +4 -3
- package/server/public/app/usage-limit-ui.js +136 -5
- package/server/public/index.html +596 -35
- package/server/relay-tools.md +4 -4
- package/server/repositories/conversation-group-repository.mjs +14 -7
- package/server/repositories/message-repository.mjs +7 -0
- package/server/repositories/session-repository.mjs +8 -1
- package/server/routes/ask-user-routes.mjs +1 -1
- package/server/routes/conversation-group-routes.mjs +3 -2
- package/server/routes/deepseek-usage-import-routes.mjs +72 -0
- package/server/routes/messages-routes.mjs +831 -36
- package/server/routes/relay-ask-routes.mjs +3 -3
- package/server/routes/relay-event-routes.mjs +2 -2
- package/server/routes/relay-schedule-routes.mjs +1 -1
- package/server/routes/remote-relay-routes.mjs +3 -3
- package/server/routes/sessions-routes.mjs +670 -63
- package/server/server-runtime.mjs +476 -26
- package/server/services/about-service.mjs +2 -0
- package/server/services/background-work-view.mjs +2 -1
- package/server/services/claude-api-settings-service.mjs +575 -0
- package/server/services/claude-api-usage-service.mjs +361 -0
- package/server/services/claude-billing-swap-service.mjs +274 -0
- package/server/services/claude-billing-switch-service.mjs +343 -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-claude-api.mjs +445 -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 +436 -0
- package/server/services/provider-model-selection.mjs +4 -2
- package/server/services/provider-reasoning-effort.mjs +24 -2
- package/server/services/relay-ask-service.mjs +1 -1
- package/server/services/relay-event-service.mjs +3 -2
- package/server/services/relay-schedule-service.mjs +1 -1
- package/server/services/remote-relay-caller-context.mjs +7 -0
- package/server/services/remote-relay-dispatcher.mjs +84 -14
- package/server/services/remote-relay-registry-service.mjs +1 -1
- package/server/services/session-worker-launch-service.mjs +294 -84
- package/server/services/session-worker-registry-service.mjs +28 -1
- package/server/services/session-worker-websocket-service.mjs +17 -0
- package/server/services/usage-limit-pause-service.mjs +55 -4
- package/server/services/worker-secret-redaction.mjs +320 -0
- package/shared/anthropic-api.mjs +202 -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 +5 -3
- package/shared/relay-asks.mjs +11 -5
- package/shared/relay-events.mjs +2 -2
- package/shared/relay-schedules.mjs +2 -2
- package/shared/remote-relay-contract.mjs +29 -9
- package/shared/remote-relay-tool-core.mjs +23 -6
- package/shared/steer-settle-failure.mjs +2 -0
- package/shared/worker-runtime/api-client.mjs +2 -2
- package/shared/worker-runtime/direct-openai-image.mjs +128 -0
- package/shared/worker-runtime/heartbeat.mjs +5 -0
- package/shared/worker-runtime/worker-log-file.mjs +60 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +6 -0
|
@@ -46,6 +46,19 @@ 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
|
+
CLAUDE_API_PROVIDER_LABEL,
|
|
52
|
+
CLAUDE_API_PROVIDER_TYPE,
|
|
53
|
+
parseClaudeApiSettingsUpdateRequest,
|
|
54
|
+
} from '../services/claude-api-settings-service.mjs';
|
|
55
|
+
import {
|
|
56
|
+
DEEPSEEK_PROVIDER_TYPE,
|
|
57
|
+
DEEPSEEK_REASONING_EFFORTS,
|
|
58
|
+
DEFAULT_DEEPSEEK_MODEL,
|
|
59
|
+
DEFAULT_DEEPSEEK_REASONING_EFFORT,
|
|
60
|
+
deepseekReasoningEffortFor,
|
|
61
|
+
} from '../../shared/deepseek-models.mjs';
|
|
49
62
|
import {
|
|
50
63
|
isWithinAllowedPrefix,
|
|
51
64
|
readWorkspaceRootPathFromBody,
|
|
@@ -133,6 +146,10 @@ import {
|
|
|
133
146
|
derivePwaShortName,
|
|
134
147
|
} from '../../shared/pwa-app-name.mjs';
|
|
135
148
|
import { parseRestartResumeUpdate } from '../services/restart-resume-service.mjs';
|
|
149
|
+
import { removeClaudeSessionLeftovers } from '../services/claude-session-leftovers.mjs';
|
|
150
|
+
import { deepseekStartingBalanceNote } from '../services/plan-usage-deepseek.mjs';
|
|
151
|
+
import { DEEPSEEK_USAGE_CHECKPOINTS_TABLE } from '../migrations/0024-deepseek-usage-totals.mjs';
|
|
152
|
+
import { claudeProjectDirSlug } from '../services/claude-session-root-service.mjs';
|
|
136
153
|
|
|
137
154
|
export { mapUsageSnapshotRow };
|
|
138
155
|
|
|
@@ -794,6 +811,131 @@ export function buildModelCatalogWithGrokProvider(modelState = {}, grokSettings
|
|
|
794
811
|
};
|
|
795
812
|
}
|
|
796
813
|
|
|
814
|
+
export function buildModelCatalogWithDeepSeekProvider(modelState = {}, deepseekSettings = {}) {
|
|
815
|
+
const configured = deepseekSettings?.enabled === true;
|
|
816
|
+
const model = String(deepseekSettings?.model || '').trim();
|
|
817
|
+
if (!configured || !model) return { ...modelState };
|
|
818
|
+
const deepseekModels = [];
|
|
819
|
+
const seenDeepSeekModels = new Set();
|
|
820
|
+
const listed = Array.isArray(deepseekSettings?.models) ? deepseekSettings.models : [];
|
|
821
|
+
for (const candidate of [model, ...listed]) {
|
|
822
|
+
const modelId = String(candidate || '').trim();
|
|
823
|
+
const lowerId = modelId.toLowerCase();
|
|
824
|
+
if (!modelId || seenDeepSeekModels.has(lowerId)) continue;
|
|
825
|
+
seenDeepSeekModels.add(lowerId);
|
|
826
|
+
deepseekModels.push(modelId);
|
|
827
|
+
}
|
|
828
|
+
const models = Array.from(new Set([...(Array.isArray(modelState?.models) ? modelState.models : []), ...deepseekModels]));
|
|
829
|
+
const reasoningByModel = { ...(modelState?.reasoningByModel || {}) };
|
|
830
|
+
const deepseekReasoningByModel = {};
|
|
831
|
+
const modelMetadataByModel = { ...(modelState?.modelMetadataByModel || {}) };
|
|
832
|
+
const providersByModel = { ...(modelState?.providersByModel || {}) };
|
|
833
|
+
// DeepSeek's ids are its own ("deepseek-*"): no other provider lists them,
|
|
834
|
+
// so the provider tag is never shared with Copilot.
|
|
835
|
+
for (const deepseekModel of deepseekModels) {
|
|
836
|
+
const lowerKey = deepseekModel.toLowerCase();
|
|
837
|
+
deepseekReasoningByModel[lowerKey] = [...DEEPSEEK_REASONING_EFFORTS];
|
|
838
|
+
if (!Array.isArray(reasoningByModel[lowerKey]) || reasoningByModel[lowerKey].length === 0) {
|
|
839
|
+
reasoningByModel[lowerKey] = [...DEEPSEEK_REASONING_EFFORTS];
|
|
840
|
+
}
|
|
841
|
+
const existingProviders = Array.isArray(providersByModel[deepseekModel])
|
|
842
|
+
? providersByModel[deepseekModel]
|
|
843
|
+
: (Array.isArray(providersByModel[lowerKey]) ? providersByModel[lowerKey] : []);
|
|
844
|
+
providersByModel[deepseekModel] = Array.from(new Set([...existingProviders, DEEPSEEK_PROVIDER_TYPE]));
|
|
845
|
+
modelMetadataByModel[deepseekModel] = {
|
|
846
|
+
...(modelMetadataByModel[deepseekModel] || {}),
|
|
847
|
+
provider: modelMetadataByModel[deepseekModel]?.provider || DEEPSEEK_PROVIDER_TYPE,
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
...modelState,
|
|
852
|
+
models,
|
|
853
|
+
reasoningByModel,
|
|
854
|
+
reasoningByProvider: {
|
|
855
|
+
...(modelState?.reasoningByProvider || {}),
|
|
856
|
+
[DEEPSEEK_PROVIDER_TYPE]: deepseekReasoningByModel,
|
|
857
|
+
},
|
|
858
|
+
modelMetadataByModel,
|
|
859
|
+
providersByModel,
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Claude API (provider `claude-api`) serves the Claude provider's model ids
|
|
865
|
+
* on an API key. Its layer tags them `claude-api` beside whatever else serves
|
|
866
|
+
* them and keeps its own efforts and context tiers: `[1m]` variants fold into
|
|
867
|
+
* their base model as a long_context tier, as on the Claude layer. An id is
|
|
868
|
+
* never enough to pick this provider; only an explicit `claude-api` does.
|
|
869
|
+
*/
|
|
870
|
+
export function buildModelCatalogWithClaudeApiProvider(modelState = {}, claudeApiSettings = {}) {
|
|
871
|
+
const configured = claudeApiSettings?.enabled === true;
|
|
872
|
+
const model = String(claudeApiSettings?.model || '').trim();
|
|
873
|
+
if (!configured || !model) return { ...modelState };
|
|
874
|
+
const listed = Array.isArray(claudeApiSettings?.models) ? claudeApiSettings.models : [];
|
|
875
|
+
const longContextBases = new Set();
|
|
876
|
+
const defaultContextBases = new Set();
|
|
877
|
+
const baseModels = [];
|
|
878
|
+
const seenBases = new Set();
|
|
879
|
+
for (const candidate of [model, ...listed]) {
|
|
880
|
+
const modelId = String(candidate || '').trim();
|
|
881
|
+
if (!modelId) continue;
|
|
882
|
+
const isLongContextVariant = CLAUDE_LONG_CONTEXT_SUFFIX_PATTERN.test(modelId);
|
|
883
|
+
const baseId = modelId.replace(CLAUDE_LONG_CONTEXT_SUFFIX_PATTERN, '');
|
|
884
|
+
if (!baseId) continue;
|
|
885
|
+
if (isLongContextVariant) longContextBases.add(baseId.toLowerCase());
|
|
886
|
+
else defaultContextBases.add(baseId.toLowerCase());
|
|
887
|
+
if (seenBases.has(baseId.toLowerCase())) continue;
|
|
888
|
+
seenBases.add(baseId.toLowerCase());
|
|
889
|
+
baseModels.push(baseId);
|
|
890
|
+
}
|
|
891
|
+
const models = Array.from(new Set([...(Array.isArray(modelState?.models) ? modelState.models : []), ...baseModels]));
|
|
892
|
+
const reasoningByModel = { ...(modelState?.reasoningByModel || {}) };
|
|
893
|
+
const claudeApiReasoningByModel = {};
|
|
894
|
+
const modelMetadataByModel = { ...(modelState?.modelMetadataByModel || {}) };
|
|
895
|
+
const providersByModel = { ...(modelState?.providersByModel || {}) };
|
|
896
|
+
const claudeApiContextTiersByModel = {};
|
|
897
|
+
const effortsByModel = claudeApiSettings?.effortsByModel && typeof claudeApiSettings.effortsByModel === 'object'
|
|
898
|
+
? claudeApiSettings.effortsByModel
|
|
899
|
+
: {};
|
|
900
|
+
for (const baseId of baseModels) {
|
|
901
|
+
const lowerKey = baseId.toLowerCase();
|
|
902
|
+
claudeApiContextTiersByModel[lowerKey] = [
|
|
903
|
+
...(defaultContextBases.has(lowerKey) ? [{ value: 'default' }] : []),
|
|
904
|
+
...(longContextBases.has(lowerKey) ? [{ value: 'long_context' }] : []),
|
|
905
|
+
];
|
|
906
|
+
const efforts = Array.isArray(effortsByModel[lowerKey]) && effortsByModel[lowerKey].length
|
|
907
|
+
? effortsByModel[lowerKey]
|
|
908
|
+
: effortsByModel[`${lowerKey}[1m]`];
|
|
909
|
+
const claudeApiEfforts = Array.isArray(efforts) && efforts.length
|
|
910
|
+
? withClaudeUltracodeTier(efforts)
|
|
911
|
+
: [...DEFAULT_CLAUDE_REASONING_EFFORTS];
|
|
912
|
+
claudeApiReasoningByModel[lowerKey] = [...claudeApiEfforts];
|
|
913
|
+
if (!Array.isArray(reasoningByModel[lowerKey]) || reasoningByModel[lowerKey].length === 0) {
|
|
914
|
+
reasoningByModel[lowerKey] = [...claudeApiEfforts];
|
|
915
|
+
}
|
|
916
|
+
const existingProviders = Array.isArray(providersByModel[baseId])
|
|
917
|
+
? providersByModel[baseId]
|
|
918
|
+
: (Array.isArray(providersByModel[lowerKey]) ? providersByModel[lowerKey] : []);
|
|
919
|
+
providersByModel[baseId] = Array.from(new Set([...existingProviders, CLAUDE_API_PROVIDER_TYPE]));
|
|
920
|
+
modelMetadataByModel[baseId] = {
|
|
921
|
+
...(modelMetadataByModel[baseId] || {}),
|
|
922
|
+
provider: modelMetadataByModel[baseId]?.provider || CLAUDE_API_PROVIDER_TYPE,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
return {
|
|
926
|
+
...modelState,
|
|
927
|
+
models,
|
|
928
|
+
reasoningByModel,
|
|
929
|
+
reasoningByProvider: {
|
|
930
|
+
...(modelState?.reasoningByProvider || {}),
|
|
931
|
+
[CLAUDE_API_PROVIDER_TYPE]: claudeApiReasoningByModel,
|
|
932
|
+
},
|
|
933
|
+
modelMetadataByModel,
|
|
934
|
+
providersByModel,
|
|
935
|
+
claudeApiContextTiersByModel,
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
|
|
797
939
|
function normalizeRequestedProviderType(value = '') {
|
|
798
940
|
const normalized = String(value || '').trim().toLowerCase();
|
|
799
941
|
if (normalized === 'openai' || normalized === 'openai-byok') return 'openai';
|
|
@@ -803,6 +945,9 @@ function normalizeRequestedProviderType(value = '') {
|
|
|
803
945
|
if (normalized === 'claude-cloud') return 'claude-cloud';
|
|
804
946
|
if (normalized === 'cursor') return 'cursor';
|
|
805
947
|
if (normalized === 'grok' || normalized === 'xai' || normalized === 'xai-grok') return 'grok';
|
|
948
|
+
if (normalized === 'deepseek') return 'deepseek';
|
|
949
|
+
// Its own provider, never read as `claude`.
|
|
950
|
+
if (normalized === 'claude-api') return 'claude-api';
|
|
806
951
|
if (normalized === 'github' || normalized === 'github-copilot') return 'github';
|
|
807
952
|
return '';
|
|
808
953
|
}
|
|
@@ -1421,6 +1566,8 @@ export function buildConversationSessionRootPayload({
|
|
|
1421
1566
|
claudeNativeSessionId = '',
|
|
1422
1567
|
workspaceRootPath = '',
|
|
1423
1568
|
resolveClaudeSessionRoot = null,
|
|
1569
|
+
// The same Claude Code layout, in the DeepSeek workers' own config dir.
|
|
1570
|
+
resolveDeepSeekSessionRoot = null,
|
|
1424
1571
|
resolveCursorSessionRoot = null,
|
|
1425
1572
|
} = {}) {
|
|
1426
1573
|
const sid = String(sdkSessionId || '').trim() || String(conversationId || '').trim();
|
|
@@ -1442,11 +1589,14 @@ export function buildConversationSessionRootPayload({
|
|
|
1442
1589
|
}
|
|
1443
1590
|
// Claude sessions have no ~/.copilot/session-state entry — only the Copilot
|
|
1444
1591
|
// CLI creates those — so they resolve against the Agent SDK's own layout and
|
|
1445
|
-
// never fall through to the branch below.
|
|
1446
|
-
|
|
1592
|
+
// never fall through to the branch below. A Claude API session is a Claude
|
|
1593
|
+
// session on an API key, in the same place.
|
|
1594
|
+
const claudeCodeProvider = String(providerType || '').trim().toLowerCase();
|
|
1595
|
+
if (claudeCodeProvider === 'claude' || claudeCodeProvider === 'deepseek' || claudeCodeProvider === 'claude-api') {
|
|
1447
1596
|
const nativeSessionId = String(claudeNativeSessionId || '').trim();
|
|
1448
|
-
|
|
1449
|
-
|
|
1597
|
+
const resolveRoot = claudeCodeProvider === 'deepseek' ? resolveDeepSeekSessionRoot : resolveClaudeSessionRoot;
|
|
1598
|
+
if (!nativeSessionId || typeof resolveRoot !== 'function') return null;
|
|
1599
|
+
const claudeRoot = resolveRoot({
|
|
1450
1600
|
claudeNativeSessionId: nativeSessionId,
|
|
1451
1601
|
workspaceRootPath: String(workspaceRootPath || '').trim(),
|
|
1452
1602
|
});
|
|
@@ -2068,6 +2218,24 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2068
2218
|
// is simply off.
|
|
2069
2219
|
claudeCloudSettingsService = null,
|
|
2070
2220
|
claudeCloudSessionService = null,
|
|
2221
|
+
// DeepSeek (provider `deepseek`): its key, models, base URL and
|
|
2222
|
+
// attribution (services/deepseek-settings-service.mjs). Absent in older
|
|
2223
|
+
// wiring and most tests, where the provider is simply off.
|
|
2224
|
+
deepseekSettingsService = null,
|
|
2225
|
+
// Claude API (provider `claude-api`): its key, models and monthly cap
|
|
2226
|
+
// (services/claude-api-settings-service.mjs). Absent in older wiring and
|
|
2227
|
+
// most tests, where the provider is simply off.
|
|
2228
|
+
claudeApiSettingsService = null,
|
|
2229
|
+
// The Claude API card and monthly cap (services/plan-usage-claude-api.mjs),
|
|
2230
|
+
// and the cap's one seam (`{ reached, error? }`; a throw counts as
|
|
2231
|
+
// reached). Absent in older wiring and most tests: no card, no cap.
|
|
2232
|
+
claudeApiPlanUsage = null,
|
|
2233
|
+
getClaudeApiCapState = () => ({ reached: false }),
|
|
2234
|
+
// Asks whether the cap flipped after the Claude API settings changed.
|
|
2235
|
+
checkClaudeApiCap = () => {},
|
|
2236
|
+
// The once-per-episode notes for stopped self-started turns on the key
|
|
2237
|
+
// (plan-usage-claude-api.mjs): a saved setting starts every chat afresh.
|
|
2238
|
+
claudeApiStopLedger = null,
|
|
2071
2239
|
reconcileUnstartedConversationProviders = async () => ({
|
|
2072
2240
|
updatedUnstartedConversations: 0,
|
|
2073
2241
|
skippedStartedConversations: 0,
|
|
@@ -2108,6 +2276,13 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2108
2276
|
// nothing while Claude Cloud is off, and absent (tests, older wiring) the
|
|
2109
2277
|
// Claude card stays the last turn's snapshot.
|
|
2110
2278
|
claudeAccountUsageService = null,
|
|
2279
|
+
// Optional DeepSeek balance (deepseek-balance-service.mjs); it reads
|
|
2280
|
+
// nothing while DeepSeek is off, and absent (tests, older wiring) the
|
|
2281
|
+
// DeepSeek card shows the relay's estimated spend alone.
|
|
2282
|
+
deepseekBalanceService = null,
|
|
2283
|
+
// Optional DeepSeek usage export (deepseek-usage-import.mjs): its figures
|
|
2284
|
+
// replace the estimate on the days it covers; absent, the card is the estimate.
|
|
2285
|
+
deepseekUsageImportService = null,
|
|
2111
2286
|
getCursorPlanAllowanceSettings = () => ({ cursorModelsUsd: null, otherModelsUsd: null, resetDay: 1 }),
|
|
2112
2287
|
setCursorPlanAllowanceSettings = () => ({ ok: false, error: 'Cursor allowance settings are unavailable' }),
|
|
2113
2288
|
getGrokPlanAllowanceSettings = () => ({ monthlyUsd: null, resetDay: 1 }),
|
|
@@ -2139,6 +2314,10 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2139
2314
|
sessionWorkerStopOverrides = null,
|
|
2140
2315
|
resolveSessionStateRoot,
|
|
2141
2316
|
resolveClaudeSessionRoot = null,
|
|
2317
|
+
resolveDeepSeekSessionRoot = null,
|
|
2318
|
+
// DeepSeek's Claude Code config directory, which the relay owns: a deleted
|
|
2319
|
+
// conversation's leftovers there go with it (claude-session-leftovers.mjs).
|
|
2320
|
+
deepseekClaudeConfigDir = '',
|
|
2142
2321
|
resolveCursorSessionRoot = null,
|
|
2143
2322
|
getTurnCeilingMinutes = () => DEFAULT_TURN_CEILING_MINUTES,
|
|
2144
2323
|
setTurnCeilingMinutes = () => ({ ok: false, error: 'Turn ceiling settings are unavailable' }),
|
|
@@ -2167,6 +2346,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2167
2346
|
hostSuspendService = null,
|
|
2168
2347
|
collectHostActivity = null,
|
|
2169
2348
|
usageLimitPauseService = null,
|
|
2349
|
+
claudeBillingSwitchService = null,
|
|
2170
2350
|
hostSuspendPlatform = process.platform,
|
|
2171
2351
|
// Remote relays (server/services/remote-relay-inbound.mjs): origin on
|
|
2172
2352
|
// conversations another relay's agent created, and the inbound switch.
|
|
@@ -2356,6 +2536,9 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2356
2536
|
FROM message_usage_snapshots
|
|
2357
2537
|
WHERE conversation_id = ?
|
|
2358
2538
|
`);
|
|
2539
|
+
// Asked on each delete: the usage service creates the table at its own
|
|
2540
|
+
// start, which may come after these routes'.
|
|
2541
|
+
const findDeepSeekUsageCheckpointsTable = db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`);
|
|
2359
2542
|
const hardDeleteConversationRows = db.transaction((conversationId) => {
|
|
2360
2543
|
if (typeof stmts.deleteConvQuestions?.run === 'function') {
|
|
2361
2544
|
stmts.deleteConvQuestions.run(conversationId);
|
|
@@ -2385,6 +2568,11 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2385
2568
|
db.prepare(`DELETE FROM queue WHERE conversation_id = ?`).run(conversationId);
|
|
2386
2569
|
db.prepare(`DELETE FROM messages WHERE conversation_id = ?`).run(conversationId);
|
|
2387
2570
|
db.prepare(`DELETE FROM runtime_sessions WHERE conversation_id = ?`).run(conversationId);
|
|
2571
|
+
// A DeepSeek conversation's usage checkpoints are only good for diffing its
|
|
2572
|
+
// later reports; the totals and days they fed stay, as history.
|
|
2573
|
+
if (findDeepSeekUsageCheckpointsTable.get(DEEPSEEK_USAGE_CHECKPOINTS_TABLE)) {
|
|
2574
|
+
db.prepare(`DELETE FROM ${DEEPSEEK_USAGE_CHECKPOINTS_TABLE} WHERE conversation_id = ?`).run(conversationId);
|
|
2575
|
+
}
|
|
2388
2576
|
// The share access log goes with it; the share rows follow the foreign key.
|
|
2389
2577
|
shares().forgetConversation(conversationId);
|
|
2390
2578
|
db.prepare(`DELETE FROM conversations WHERE id = ?`).run(conversationId);
|
|
@@ -2410,17 +2598,33 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2410
2598
|
const sessionLaunchMutex = createKeyedMutex();
|
|
2411
2599
|
const relaunchCoalescer = createRelaunchCoalescer({ windowMs: RELAUNCH_COALESCE_WINDOW_MS });
|
|
2412
2600
|
|
|
2601
|
+
function getDeepSeekProviderSettings() {
|
|
2602
|
+
return deepseekSettingsService?.getSettings?.()
|
|
2603
|
+
|| { configured: false, enabled: false, model: DEFAULT_DEEPSEEK_MODEL, models: [], availableModels: [] };
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
function getClaudeApiProviderSettings() {
|
|
2607
|
+
return claudeApiSettingsService?.getSettings?.()
|
|
2608
|
+
|| { configured: false, enabled: false, model: '', models: [], availableModels: [], effortsByModel: {} };
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2413
2611
|
// Layer every enabled provider's models onto the base Copilot catalog.
|
|
2414
2612
|
function buildModelCatalogWithProviders(modelState, openAISettings = getOpenAIProviderSettings()) {
|
|
2415
|
-
return
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2613
|
+
return buildModelCatalogWithClaudeApiProvider(
|
|
2614
|
+
buildModelCatalogWithDeepSeekProvider(
|
|
2615
|
+
buildModelCatalogWithGrokProvider(
|
|
2616
|
+
buildModelCatalogWithCursorProvider(
|
|
2617
|
+
buildModelCatalogWithClaudeProvider(
|
|
2618
|
+
buildModelCatalogWithOpenAIProvider(modelState, openAISettings),
|
|
2619
|
+
getClaudeProviderSettings(),
|
|
2620
|
+
),
|
|
2621
|
+
getCursorProviderSettings(),
|
|
2622
|
+
),
|
|
2623
|
+
getGrokProviderSettings(),
|
|
2420
2624
|
),
|
|
2421
|
-
|
|
2625
|
+
getDeepSeekProviderSettings(),
|
|
2422
2626
|
),
|
|
2423
|
-
|
|
2627
|
+
getClaudeApiProviderSettings(),
|
|
2424
2628
|
);
|
|
2425
2629
|
}
|
|
2426
2630
|
|
|
@@ -2536,10 +2740,10 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2536
2740
|
* it. Nothing else in the project folder is touched — it holds every other
|
|
2537
2741
|
* session of that workspace and the project memory.
|
|
2538
2742
|
*/
|
|
2539
|
-
function deleteClaudeCliSession({ conversation, runtimeSession }) {
|
|
2743
|
+
function deleteClaudeCliSession({ conversation, runtimeSession, resolveRoot = resolveClaudeSessionRoot }) {
|
|
2540
2744
|
const nativeSessionId = String(runtimeSession?.claude_native_session_id || '').trim();
|
|
2541
|
-
if (!nativeSessionId || typeof
|
|
2542
|
-
const anchors =
|
|
2745
|
+
if (!nativeSessionId || typeof resolveRoot !== 'function') return { deleted: null };
|
|
2746
|
+
const anchors = resolveRoot({
|
|
2543
2747
|
claudeNativeSessionId: nativeSessionId,
|
|
2544
2748
|
workspaceRootPath: String(
|
|
2545
2749
|
conversation?.runtime_workspace_root_path || conversation?.configured_workspace_root_path || '',
|
|
@@ -2553,7 +2757,25 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2553
2757
|
.filter((target) => path.dirname(target) === projectDir
|
|
2554
2758
|
&& [nativeSessionId, `${nativeSessionId}.jsonl`].includes(path.basename(target)));
|
|
2555
2759
|
for (const target of targets) fs.rmSync(target, { recursive: true, force: true });
|
|
2556
|
-
return { deleted: targets.length > 0 };
|
|
2760
|
+
return { deleted: targets.length > 0, projectDirPath: projectDir };
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
/**
|
|
2764
|
+
* A DeepSeek conversation's Claude Code session: the transcript as for
|
|
2765
|
+
* Claude, then what the killed CLI left in the relay's own config directory
|
|
2766
|
+
* (its hook environment, registration, shell snapshot, and the project
|
|
2767
|
+
* folder once no session is left in it).
|
|
2768
|
+
*/
|
|
2769
|
+
function deleteDeepSeekCliSession({ conversation, runtimeSession }) {
|
|
2770
|
+
const outcome = deleteClaudeCliSession({ conversation, runtimeSession, resolveRoot: resolveDeepSeekSessionRoot });
|
|
2771
|
+
const configRoot = String(deepseekClaudeConfigDir || '').trim();
|
|
2772
|
+
const nativeSessionId = String(runtimeSession?.claude_native_session_id || '').trim();
|
|
2773
|
+
if (!configRoot || !nativeSessionId) return { deleted: outcome.deleted };
|
|
2774
|
+
const workspaceRoot = String(conversation?.runtime_workspace_root_path || conversation?.configured_workspace_root_path || '').trim();
|
|
2775
|
+
const projectDirPath = outcome.projectDirPath
|
|
2776
|
+
|| (workspaceRoot ? path.join(configRoot, 'projects', claudeProjectDirSlug(workspaceRoot)) : '');
|
|
2777
|
+
const removed = removeClaudeSessionLeftovers({ configRoot, nativeSessionId, projectDirPath });
|
|
2778
|
+
return { deleted: outcome.deleted === null && removed.length ? true : outcome.deleted };
|
|
2557
2779
|
}
|
|
2558
2780
|
|
|
2559
2781
|
/**
|
|
@@ -2593,7 +2815,8 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2593
2815
|
async function deleteCliSession({ conversation, conversationId, sdkSessionId, runtimeSession }) {
|
|
2594
2816
|
const provider = String(runtimeSession?.provider_type || '').trim().toLowerCase();
|
|
2595
2817
|
try {
|
|
2596
|
-
if (provider === 'claude') return deleteClaudeCliSession({ conversation, runtimeSession });
|
|
2818
|
+
if (provider === 'claude' || provider === 'claude-api') return deleteClaudeCliSession({ conversation, runtimeSession });
|
|
2819
|
+
if (provider === 'deepseek') return deleteDeepSeekCliSession({ conversation, runtimeSession });
|
|
2597
2820
|
if (!sdkSessionId) return { deleted: null };
|
|
2598
2821
|
if (!provider || provider === 'github' || provider === 'openai') {
|
|
2599
2822
|
return await deleteCopilotCliSession({ sdkSessionId, conversationId });
|
|
@@ -2666,7 +2889,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
2666
2889
|
/**
|
|
2667
2890
|
* The `group` field of a single conversation payload: the summary and the
|
|
2668
2891
|
* conversation's role. The live members, with what each is doing, only
|
|
2669
|
-
* when asked for (`groupMembers=1`, the
|
|
2892
|
+
* when asked for (`groupMembers=1`, the relay_tool's read_session)
|
|
2670
2893
|
* and never for a paired relay's agent: the browser does not read them,
|
|
2671
2894
|
* and archived members' titles stay on this relay.
|
|
2672
2895
|
*/
|
|
@@ -3527,9 +3750,12 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
3527
3750
|
const providerType = String(runtimeSession?.provider_type || 'github').trim().toLowerCase();
|
|
3528
3751
|
// The cursor and grok workers post the same Claude-shaped usage blob, and
|
|
3529
3752
|
// so does the Claude Cloud worker (the cloud session's context and token
|
|
3530
|
-
// usage), so
|
|
3753
|
+
// usage), and so does the DeepSeek worker (the Claude one, pointed at
|
|
3754
|
+
// DeepSeek), and so does the Claude API one (the Claude worker on an API
|
|
3755
|
+
// key), so all six providers share the stored reader.
|
|
3531
3756
|
const usesStoredContextUsage = providerType === 'claude' || providerType === 'cursor'
|
|
3532
|
-
|| providerType === 'grok' || providerType === 'claude-cloud'
|
|
3757
|
+
|| providerType === 'grok' || providerType === 'claude-cloud' || providerType === 'deepseek'
|
|
3758
|
+
|| providerType === 'claude-api';
|
|
3533
3759
|
|
|
3534
3760
|
const parsed = usesStoredContextUsage
|
|
3535
3761
|
? (() => {
|
|
@@ -3558,11 +3784,12 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
3558
3784
|
String(runtimeSession?.conversation_id || '').trim() || lookupId,
|
|
3559
3785
|
);
|
|
3560
3786
|
const autoCompactWindow = parseAutoCompactWindow(conversationRow?.auto_compact_window);
|
|
3561
|
-
// Only the local Claude worker
|
|
3562
|
-
//
|
|
3787
|
+
// Only the local Claude worker (and the DeepSeek and Claude API ones, the
|
|
3788
|
+
// same runner) runs a CLI whose window the slider sets; it reports that
|
|
3789
|
+
// CLI on its heartbeat's steering snapshot.
|
|
3563
3790
|
const windowStatus = resolveAutoCompactWindowStatus({
|
|
3564
3791
|
storedWindow: autoCompactWindow,
|
|
3565
|
-
steering: providerType === 'claude' && copilotSessionId
|
|
3792
|
+
steering: (providerType === 'claude' || providerType === 'deepseek' || providerType === 'claude-api') && copilotSessionId
|
|
3566
3793
|
? sessionWorkerRegistry?.getWorker?.(copilotSessionId)?.steering || null
|
|
3567
3794
|
: null,
|
|
3568
3795
|
});
|
|
@@ -3582,7 +3809,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
3582
3809
|
// What this folder's Claude commits say (the 🧠 modal's override row).
|
|
3583
3810
|
// A Claude Cloud chat has the row when it was started from a folder,
|
|
3584
3811
|
// with its own model in the example.
|
|
3585
|
-
...(providerType === 'claude'
|
|
3812
|
+
...(providerType === 'claude' || providerType === 'claude-api'
|
|
3586
3813
|
? { attribution: workspaceRootAttributionFor(conversationRow) }
|
|
3587
3814
|
: {}),
|
|
3588
3815
|
...(providerType === 'claude-cloud' && cloudConversationFolder(conversationRow)
|
|
@@ -3813,6 +4040,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
3813
4040
|
claudeNativeSessionId: runtimeSession?.claude_native_session_id || '',
|
|
3814
4041
|
workspaceRootPath: workspaceState?.currentWorkspaceRootPath || '',
|
|
3815
4042
|
resolveClaudeSessionRoot,
|
|
4043
|
+
resolveDeepSeekSessionRoot,
|
|
3816
4044
|
resolveCursorSessionRoot,
|
|
3817
4045
|
});
|
|
3818
4046
|
const dbMessages = stmts.getMessages.all(conversationId);
|
|
@@ -4486,6 +4714,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
4486
4714
|
claudeNativeSessionId: runtimeSession?.claude_native_session_id || '',
|
|
4487
4715
|
workspaceRootPath: workspaceState?.currentWorkspaceRootPath || '',
|
|
4488
4716
|
resolveClaudeSessionRoot,
|
|
4717
|
+
resolveDeepSeekSessionRoot,
|
|
4489
4718
|
resolveCursorSessionRoot,
|
|
4490
4719
|
});
|
|
4491
4720
|
const historyCursor = { limit, beforeMessageId, beforeTimestamp, afterMessageId, afterTimestamp, aroundMessageId };
|
|
@@ -4608,6 +4837,9 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
4608
4837
|
try { return getTaskStallMinutes(); } catch { return DEFAULT_TASK_STALL_MINUTES; }
|
|
4609
4838
|
})(),
|
|
4610
4839
|
usageLimitPause: usageLimitPauseService?.getPause?.(resolvedConversationId) || null,
|
|
4840
|
+
// A Claude chat's billing (subscription or API key) and whether the
|
|
4841
|
+
// composer's toggle may switch it now, with the reason when not.
|
|
4842
|
+
billing: claudeBillingSwitchService?.billingState?.(resolvedConversationId) || null,
|
|
4611
4843
|
previews: listConversationPreviews?.(resolvedConversationId) || [],
|
|
4612
4844
|
// Every pin of the conversation, also those outside this history page.
|
|
4613
4845
|
pins: listConversationPins(resolvedConversationId),
|
|
@@ -4894,7 +5126,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
4894
5126
|
return res.json({ ok: true, ...persisted });
|
|
4895
5127
|
});
|
|
4896
5128
|
|
|
4897
|
-
// ─── PATCH /api/conversation/:id/settings — the
|
|
5129
|
+
// ─── PATCH /api/conversation/:id/settings — the relay_tool's update_session ───
|
|
4898
5130
|
//
|
|
4899
5131
|
// `{ settings: { model, effort, mode, auto_compact_window, title, group,
|
|
4900
5132
|
// thinking_enabled, thinking_display, cwd }, dry_run, owner_approved }`.
|
|
@@ -5015,7 +5247,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
5015
5247
|
plan.preferences.thinkingDisplay = wanted.thinking_display;
|
|
5016
5248
|
change('thinking_display');
|
|
5017
5249
|
}
|
|
5018
|
-
if (provider !== 'claude' && ['auto_compact_window', 'thinking_enabled', 'thinking_display'].some((key) => wanted[key] !== undefined)) {
|
|
5250
|
+
if (provider !== 'claude' && provider !== 'claude-api' && ['auto_compact_window', 'thinking_enabled', 'thinking_display'].some((key) => wanted[key] !== undefined)) {
|
|
5019
5251
|
plan.notes.push(`auto_compact_window and thinking apply to Claude sessions only; this one runs on ${provider}, so they are stored but change nothing there.`);
|
|
5020
5252
|
}
|
|
5021
5253
|
if (wanted.title !== undefined) {
|
|
@@ -6147,6 +6379,35 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6147
6379
|
environmentId: cloudEnvironmentId,
|
|
6148
6380
|
});
|
|
6149
6381
|
}
|
|
6382
|
+
// Claude API is asked for by name only, like Claude Cloud: its model ids
|
|
6383
|
+
// are the Claude provider's own, so a model never selects it. Refused
|
|
6384
|
+
// first while it has no key or is switched off.
|
|
6385
|
+
const useClaudeApiProvider = requestedProviderType === CLAUDE_API_PROVIDER_TYPE;
|
|
6386
|
+
const claudeApiSettings = useClaudeApiProvider ? getClaudeApiProviderSettings() : null;
|
|
6387
|
+
if (useClaudeApiProvider && claudeApiSettings?.enabled !== true) {
|
|
6388
|
+
return res.status(400).json({
|
|
6389
|
+
ok: false,
|
|
6390
|
+
error: claudeApiSettings?.configured === true
|
|
6391
|
+
? 'Claude API is switched off for new conversations.'
|
|
6392
|
+
: 'Claude API key is not configured.',
|
|
6393
|
+
code: 'CLAUDE_API_NOT_CONFIGURED',
|
|
6394
|
+
});
|
|
6395
|
+
}
|
|
6396
|
+
// This month's cap (plan decision 12): no new Claude API chat while it is
|
|
6397
|
+
// reached, for New Chat and an agent's create_session alike. A cap that
|
|
6398
|
+
// cannot be read counts as reached.
|
|
6399
|
+
if (useClaudeApiProvider) {
|
|
6400
|
+
let cap = null;
|
|
6401
|
+
try { cap = getClaudeApiCapState() || null; } catch { cap = { reached: true }; }
|
|
6402
|
+
if (cap?.reached === true) {
|
|
6403
|
+
return res.status(409).json({
|
|
6404
|
+
ok: false,
|
|
6405
|
+
// The remedy names the place, so the reason does not.
|
|
6406
|
+
error: `${String(cap.reason || cap.error || '').trim() || 'This month\'s Claude API spend has reached its cap.'} No Claude API chat was started; start it on the Claude subscription, or raise the cap in Settings → Providers → Claude API.`,
|
|
6407
|
+
code: 'CLAUDE_API_CAP_REACHED',
|
|
6408
|
+
});
|
|
6409
|
+
}
|
|
6410
|
+
}
|
|
6150
6411
|
const modelState = buildModelCatalogWithProviders(
|
|
6151
6412
|
getModelCatalogState(),
|
|
6152
6413
|
openAISettings,
|
|
@@ -6186,6 +6447,19 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6186
6447
|
...(Array.isArray(grokSettings?.models) ? grokSettings.models : []),
|
|
6187
6448
|
].map((value) => String(value || '').trim()).filter(Boolean);
|
|
6188
6449
|
const availableGrokModels = new Set(grokModelCatalog.map((value) => value.toLowerCase()));
|
|
6450
|
+
const deepseekSettings = getDeepSeekProviderSettings();
|
|
6451
|
+
const deepseekModelCatalog = [
|
|
6452
|
+
String(deepseekSettings?.model || '').trim(),
|
|
6453
|
+
...(Array.isArray(deepseekSettings?.models) ? deepseekSettings.models : []),
|
|
6454
|
+
].map((value) => String(value || '').trim()).filter(Boolean);
|
|
6455
|
+
const availableDeepSeekModels = new Set(deepseekModelCatalog.map((value) => value.toLowerCase()));
|
|
6456
|
+
// Base ids of "[1m]" variants count too, as for the Claude provider.
|
|
6457
|
+
const availableClaudeApiModels = new Set((useClaudeApiProvider ? [
|
|
6458
|
+
String(claudeApiSettings?.model || '').trim(),
|
|
6459
|
+
...(Array.isArray(claudeApiSettings?.models) ? claudeApiSettings.models : []),
|
|
6460
|
+
] : []).map((value) => String(value || '').trim()).filter(Boolean)
|
|
6461
|
+
.flatMap((value) => [value, value.replace(CLAUDE_LONG_CONTEXT_SUFFIX_PATTERN, '')])
|
|
6462
|
+
.filter(Boolean));
|
|
6189
6463
|
const useOpenAIProvider = requestedProviderType === 'openai'
|
|
6190
6464
|
|| requestedProviderType === 'openai-image'
|
|
6191
6465
|
|| (
|
|
@@ -6221,6 +6495,15 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6221
6495
|
&& availableGrokModels.has(requestedBootstrapModel.toLowerCase())
|
|
6222
6496
|
)
|
|
6223
6497
|
);
|
|
6498
|
+
const useDeepSeekProvider = !useOpenAIProvider && !useClaudeProvider && !useCursorProvider && !useGrokProvider && (
|
|
6499
|
+
requestedProviderType === 'deepseek'
|
|
6500
|
+
|| (
|
|
6501
|
+
requestedProviderType === ''
|
|
6502
|
+
&& requestedBootstrapModel
|
|
6503
|
+
&& requestedBootstrapModel.toLowerCase() !== 'auto'
|
|
6504
|
+
&& availableDeepSeekModels.has(requestedBootstrapModel.toLowerCase())
|
|
6505
|
+
)
|
|
6506
|
+
);
|
|
6224
6507
|
// Managed providers resolve the request against their own catalog only.
|
|
6225
6508
|
// Routing it through the merged Copilot catalog first (resolveBootstrapModelSelection)
|
|
6226
6509
|
// silently substituted that catalog's current model whenever the requested
|
|
@@ -6241,6 +6524,12 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6241
6524
|
configuredModel: claudeCloudSettings.defaultModel,
|
|
6242
6525
|
availableModels: Array.isArray(claudeCloudSettings.models) ? claudeCloudSettings.models : [],
|
|
6243
6526
|
})
|
|
6527
|
+
: useClaudeApiProvider
|
|
6528
|
+
? resolveProviderModelSelection({
|
|
6529
|
+
requestedModel: providerRequestedModel(availableClaudeApiModels),
|
|
6530
|
+
configuredModel: claudeApiSettings.model,
|
|
6531
|
+
availableModels: availableClaudeApiModels,
|
|
6532
|
+
})
|
|
6244
6533
|
: useOpenAIProvider
|
|
6245
6534
|
? resolveProviderModelSelection({
|
|
6246
6535
|
requestedModel: providerRequestedModel(availableOpenAIModels),
|
|
@@ -6266,23 +6555,35 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6266
6555
|
configuredModel: grokSettings.model,
|
|
6267
6556
|
availableModels: grokModelCatalog,
|
|
6268
6557
|
})
|
|
6269
|
-
:
|
|
6558
|
+
: (useDeepSeekProvider
|
|
6559
|
+
? resolveProviderModelSelection({
|
|
6560
|
+
requestedModel: providerRequestedModel(deepseekModelCatalog),
|
|
6561
|
+
configuredModel: deepseekSettings.model,
|
|
6562
|
+
availableModels: deepseekModelCatalog,
|
|
6563
|
+
})
|
|
6564
|
+
: null))));
|
|
6270
6565
|
// The single rejection for "this provider does not offer that model".
|
|
6271
6566
|
// Per-provider pre-checks used to duplicate it and had drifted apart in
|
|
6272
6567
|
// wording, casing rules and whether they listed the alternatives.
|
|
6273
6568
|
if (providerModelSelection && !providerModelSelection.ok) {
|
|
6274
6569
|
const providerLabel = useClaudeCloudProvider
|
|
6275
6570
|
? 'Claude Cloud'
|
|
6571
|
+
: useClaudeApiProvider
|
|
6572
|
+
? CLAUDE_API_PROVIDER_LABEL
|
|
6276
6573
|
: (useOpenAIProvider
|
|
6277
6574
|
? 'OpenAI'
|
|
6278
|
-
: (useClaudeProvider ? 'Claude' : (useCursorProvider ? 'Cursor' : 'Grok')));
|
|
6575
|
+
: (useClaudeProvider ? 'Claude' : (useCursorProvider ? 'Cursor' : (useGrokProvider ? 'Grok' : 'DeepSeek'))));
|
|
6279
6576
|
const providerCode = useClaudeCloudProvider
|
|
6280
6577
|
? 'CLAUDE_CLOUD_MODEL_UNAVAILABLE'
|
|
6578
|
+
: useClaudeApiProvider
|
|
6579
|
+
? 'CLAUDE_API_MODEL_UNAVAILABLE'
|
|
6281
6580
|
: (useOpenAIProvider
|
|
6282
6581
|
? 'OPENAI_MODEL_UNAVAILABLE'
|
|
6283
6582
|
: (useClaudeProvider
|
|
6284
6583
|
? 'CLAUDE_MODEL_UNAVAILABLE'
|
|
6285
|
-
: (useCursorProvider
|
|
6584
|
+
: (useCursorProvider
|
|
6585
|
+
? 'CURSOR_MODEL_UNAVAILABLE'
|
|
6586
|
+
: (useGrokProvider ? 'GROK_MODEL_UNAVAILABLE' : 'DEEPSEEK_MODEL_UNAVAILABLE'))));
|
|
6286
6587
|
return res.status(400).json({
|
|
6287
6588
|
ok: false,
|
|
6288
6589
|
error: `${providerLabel} model "${providerModelSelection.requestedModel}" is not available`,
|
|
@@ -6302,6 +6603,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6302
6603
|
if (
|
|
6303
6604
|
remoteRequest.remote
|
|
6304
6605
|
&& !useClaudeCloudProvider && !useOpenAIProvider && !useClaudeProvider && !useCursorProvider && !useGrokProvider
|
|
6606
|
+
&& !useDeepSeekProvider && !useClaudeApiProvider
|
|
6305
6607
|
) {
|
|
6306
6608
|
const mergedModels = Array.isArray(modelState?.models) ? modelState.models : [];
|
|
6307
6609
|
const providersByModel = modelState?.providersByModel && typeof modelState.providersByModel === 'object'
|
|
@@ -6367,6 +6669,15 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6367
6669
|
code: 'GROK_NOT_CONFIGURED',
|
|
6368
6670
|
});
|
|
6369
6671
|
}
|
|
6672
|
+
if (useDeepSeekProvider && deepseekSettings?.enabled !== true) {
|
|
6673
|
+
return res.status(400).json({
|
|
6674
|
+
ok: false,
|
|
6675
|
+
error: deepseekSettings?.configured === true
|
|
6676
|
+
? 'DeepSeek is switched off for new conversations'
|
|
6677
|
+
: 'DeepSeek API key is not configured',
|
|
6678
|
+
code: 'DEEPSEEK_NOT_CONFIGURED',
|
|
6679
|
+
});
|
|
6680
|
+
}
|
|
6370
6681
|
|
|
6371
6682
|
// The session group an agent's create_session names (an id or an exact
|
|
6372
6683
|
// name): resolved before anything is written, so an unknown one creates
|
|
@@ -6421,7 +6732,10 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6421
6732
|
code: 'OPENAI_IMAGE_MODEL_REQUIRED',
|
|
6422
6733
|
});
|
|
6423
6734
|
}
|
|
6424
|
-
if (
|
|
6735
|
+
if (
|
|
6736
|
+
!useClaudeCloudProvider && !useOpenAIProvider && !useClaudeProvider && !useCursorProvider && !useGrokProvider
|
|
6737
|
+
&& !useDeepSeekProvider && !useClaudeApiProvider
|
|
6738
|
+
) {
|
|
6425
6739
|
const selectedProviders = Array.isArray(modelState?.providersByModel?.[String(selectedModel || '').trim().toLowerCase()])
|
|
6426
6740
|
? modelState.providersByModel[String(selectedModel || '').trim().toLowerCase()]
|
|
6427
6741
|
: [];
|
|
@@ -6434,8 +6748,12 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6434
6748
|
code: 'OPENAI_PROVIDER_REQUIRED',
|
|
6435
6749
|
});
|
|
6436
6750
|
}
|
|
6437
|
-
|
|
6438
|
-
|
|
6751
|
+
// Claude API serves the Claude provider's ids: a model both list is
|
|
6752
|
+
// still Claude's, and one only Claude API lists names that provider.
|
|
6753
|
+
const selectedProviderIds = selectedProviders.map((provider) => String(provider || '').trim().toLowerCase());
|
|
6754
|
+
const claudeOnlySelection = selectedProviderIds.length > 0
|
|
6755
|
+
&& selectedProviderIds.includes('claude')
|
|
6756
|
+
&& selectedProviderIds.every((provider) => provider === 'claude' || provider === CLAUDE_API_PROVIDER_TYPE);
|
|
6439
6757
|
if (claudeOnlySelection) {
|
|
6440
6758
|
return res.status(400).json({
|
|
6441
6759
|
ok: false,
|
|
@@ -6443,6 +6761,15 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6443
6761
|
code: 'CLAUDE_PROVIDER_REQUIRED',
|
|
6444
6762
|
});
|
|
6445
6763
|
}
|
|
6764
|
+
const claudeApiOnlySelection = selectedProviderIds.length > 0
|
|
6765
|
+
&& selectedProviderIds.every((provider) => provider === CLAUDE_API_PROVIDER_TYPE);
|
|
6766
|
+
if (claudeApiOnlySelection) {
|
|
6767
|
+
return res.status(400).json({
|
|
6768
|
+
ok: false,
|
|
6769
|
+
error: `Model "${selectedModel}" requires the Claude API provider`,
|
|
6770
|
+
code: 'CLAUDE_API_PROVIDER_REQUIRED',
|
|
6771
|
+
});
|
|
6772
|
+
}
|
|
6446
6773
|
const cursorOnlySelection = selectedProviders.length > 0
|
|
6447
6774
|
&& selectedProviders.every((provider) => String(provider || '').trim().toLowerCase() === 'cursor');
|
|
6448
6775
|
if (cursorOnlySelection) {
|
|
@@ -6461,17 +6788,28 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6461
6788
|
code: 'GROK_PROVIDER_REQUIRED',
|
|
6462
6789
|
});
|
|
6463
6790
|
}
|
|
6791
|
+
const deepseekOnlySelection = selectedProviders.length > 0
|
|
6792
|
+
&& selectedProviders.every((provider) => String(provider || '').trim().toLowerCase() === DEEPSEEK_PROVIDER_TYPE);
|
|
6793
|
+
if (deepseekOnlySelection) {
|
|
6794
|
+
return res.status(400).json({
|
|
6795
|
+
ok: false,
|
|
6796
|
+
error: `Model "${selectedModel}" requires the DeepSeek provider`,
|
|
6797
|
+
code: 'DEEPSEEK_PROVIDER_REQUIRED',
|
|
6798
|
+
});
|
|
6799
|
+
}
|
|
6464
6800
|
}
|
|
6465
6801
|
|
|
6466
6802
|
const bootstrapProviderType = useClaudeCloudProvider
|
|
6467
6803
|
? 'claude-cloud'
|
|
6804
|
+
: useClaudeApiProvider
|
|
6805
|
+
? CLAUDE_API_PROVIDER_TYPE
|
|
6468
6806
|
: (useOpenAIProvider
|
|
6469
6807
|
? 'openai'
|
|
6470
6808
|
: (useClaudeProvider
|
|
6471
6809
|
? 'claude'
|
|
6472
6810
|
: (useCursorProvider
|
|
6473
6811
|
? 'cursor'
|
|
6474
|
-
: (useGrokProvider ? 'grok' : 'github'))));
|
|
6812
|
+
: (useGrokProvider ? 'grok' : (useDeepSeekProvider ? 'deepseek' : 'github')))));
|
|
6475
6813
|
// Preferences are written in the same transaction as the conversation, so
|
|
6476
6814
|
// the caller's current mode has to arrive with the request: without it every
|
|
6477
6815
|
// new chat would be stored as the default one and reset the composer. An
|
|
@@ -6487,11 +6825,15 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6487
6825
|
const bootstrapReasoning = resolveProviderReasoningEffort({
|
|
6488
6826
|
requestedEffort: req.body?.reasoningEffort || copilotVariantEffort,
|
|
6489
6827
|
strict: bootstrapProviderType === 'openai',
|
|
6828
|
+
...(bootstrapProviderType === 'deepseek'
|
|
6829
|
+
? { defaultEffort: DEFAULT_DEEPSEEK_REASONING_EFFORT, aliasFor: deepseekReasoningEffortFor }
|
|
6830
|
+
: {}),
|
|
6490
6831
|
supportedEfforts: supportedReasoningEffortsForProviderModel({
|
|
6491
6832
|
providerType: bootstrapProviderType,
|
|
6492
6833
|
model: selectedModel,
|
|
6493
6834
|
cursorSettings,
|
|
6494
6835
|
claudeSettings,
|
|
6836
|
+
claudeApiSettings,
|
|
6495
6837
|
grokSettings,
|
|
6496
6838
|
reasoningByModel: modelState?.reasoningByModel || null,
|
|
6497
6839
|
}),
|
|
@@ -6536,7 +6878,10 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
6536
6878
|
{
|
|
6537
6879
|
assignConfiguredProvider: true,
|
|
6538
6880
|
providerType: bootstrapProviderType,
|
|
6539
|
-
providerModel: (
|
|
6881
|
+
providerModel: (
|
|
6882
|
+
useClaudeCloudProvider || useOpenAIProvider || useClaudeProvider || useCursorProvider || useGrokProvider
|
|
6883
|
+
|| useDeepSeekProvider || useClaudeApiProvider
|
|
6884
|
+
) ? selectedModel : null,
|
|
6540
6885
|
},
|
|
6541
6886
|
);
|
|
6542
6887
|
// What the cloud session will clone, stored with the conversation so
|
|
@@ -7511,6 +7856,53 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7511
7856
|
});
|
|
7512
7857
|
});
|
|
7513
7858
|
|
|
7859
|
+
// A rotated API key only reaches a worker's environment at spawn: live
|
|
7860
|
+
// workers of the provider keep the old key and fail each turn after the
|
|
7861
|
+
// rotation. Stops them; the primer or the next delivery respawns them with
|
|
7862
|
+
// the fresh key from settings.
|
|
7863
|
+
//
|
|
7864
|
+
// The workers are picked by what they run: a worker that states its kind
|
|
7865
|
+
// is taken at its word (a Claude chat switched to the API key may still
|
|
7866
|
+
// have its subscription worker finishing a turn, and one switched back
|
|
7867
|
+
// its API worker); one that does not is taken at its chat's provider.
|
|
7868
|
+
async function stopLiveProviderWorkersForKeyRotation(providerType) {
|
|
7869
|
+
const rows = db.prepare(`
|
|
7870
|
+
SELECT DISTINCT NULLIF(sdk_session_id, '') AS sdk_session_id
|
|
7871
|
+
FROM runtime_sessions
|
|
7872
|
+
WHERE LOWER(COALESCE(provider_type, '')) = ?
|
|
7873
|
+
AND NULLIF(sdk_session_id, '') IS NOT NULL
|
|
7874
|
+
`).all(providerType);
|
|
7875
|
+
const sids = new Set();
|
|
7876
|
+
for (const worker of sessionWorkerRegistry?.listWorkers?.() || []) {
|
|
7877
|
+
if (worker?.kind === providerType && worker.sdkSessionId) sids.add(worker.sdkSessionId);
|
|
7878
|
+
}
|
|
7879
|
+
for (const row of rows) {
|
|
7880
|
+
const sid = String(row.sdk_session_id || '').trim();
|
|
7881
|
+
const kind = sid ? (sessionWorkerRegistry?.getWorker?.(sid)?.kind || null) : null;
|
|
7882
|
+
if (sid && (!kind || kind === providerType)) sids.add(sid);
|
|
7883
|
+
}
|
|
7884
|
+
for (const sid of sids) {
|
|
7885
|
+
if (!sid) continue;
|
|
7886
|
+
const workerState = sessionWorkerSupervisor?.getWorkerState?.(sid) || null;
|
|
7887
|
+
if (!workerState) continue;
|
|
7888
|
+
try {
|
|
7889
|
+
const stopped = await stopIdleWorkspaceRootSession({
|
|
7890
|
+
sdkSessionId: sid,
|
|
7891
|
+
worker: sessionWorkerRegistry?.getWorker?.(sid) || null,
|
|
7892
|
+
sessionWorkerSupervisor,
|
|
7893
|
+
sessionWorkerRegistry,
|
|
7894
|
+
sessionWorkerProcessInspector,
|
|
7895
|
+
});
|
|
7896
|
+
if (stopped.ok) {
|
|
7897
|
+
sessionWorkerSupervisor?.clearRestartSchedule?.(sid, { resetKilledMarker: true });
|
|
7898
|
+
console.log(`[sessions] ${providerType} worker ${sid.slice(0, 8)} stopped for API key rotation`);
|
|
7899
|
+
}
|
|
7900
|
+
} catch (error) {
|
|
7901
|
+
console.warn(`[sessions] ${providerType} worker ${sid.slice(0, 8)} stop failed on key rotation: ${error?.message || error}`);
|
|
7902
|
+
}
|
|
7903
|
+
}
|
|
7904
|
+
}
|
|
7905
|
+
|
|
7514
7906
|
app.get('/api/settings/cursor', auth, (_req, res) => {
|
|
7515
7907
|
const settings = getCursorProviderSettings();
|
|
7516
7908
|
return res.json({
|
|
@@ -7542,33 +7934,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7542
7934
|
// fail each turn after the rotation. Stop them now; the primer or the
|
|
7543
7935
|
// next delivery respawns them with the fresh key from settings.
|
|
7544
7936
|
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
|
-
}
|
|
7937
|
+
await stopLiveProviderWorkersForKeyRotation('cursor');
|
|
7572
7938
|
}
|
|
7573
7939
|
const shouldDiscover = result.enabled === true && (
|
|
7574
7940
|
!!parsed.apiKey
|
|
@@ -7626,6 +7992,164 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
7626
7992
|
});
|
|
7627
7993
|
});
|
|
7628
7994
|
|
|
7995
|
+
// --- DeepSeek ----------------------------------------------------------
|
|
7996
|
+
// The key is written here and never read back: GET and every answer carry
|
|
7997
|
+
// `configured` only. A rotated key reaches a worker only at spawn, so the
|
|
7998
|
+
// live DeepSeek workers are stopped; the next message starts them again.
|
|
7999
|
+
// What the tab shows: the stored settings, and whether the starting
|
|
8000
|
+
// balance still applies (a top-up since it was set replaces it), judged
|
|
8001
|
+
// on the currency the card's bar uses: the last balance read's main one.
|
|
8002
|
+
function describeDeepSeekSettings() {
|
|
8003
|
+
const described = deepseekSettingsService.describe();
|
|
8004
|
+
let references = [];
|
|
8005
|
+
try {
|
|
8006
|
+
references = planUsageService?.readDeepSeekBalanceReferences?.() || [];
|
|
8007
|
+
} catch {}
|
|
8008
|
+
let balances = null;
|
|
8009
|
+
try {
|
|
8010
|
+
balances = deepseekBalanceService?.peekBalance?.()?.balances || null;
|
|
8011
|
+
} catch {}
|
|
8012
|
+
const startingBalanceNote = deepseekStartingBalanceNote(
|
|
8013
|
+
described.startingBalance ? { amount: described.startingBalance, setAt: described.startingBalanceSetAt } : null,
|
|
8014
|
+
references,
|
|
8015
|
+
{ balances },
|
|
8016
|
+
);
|
|
8017
|
+
return { ...described, startingBalanceNote };
|
|
8018
|
+
}
|
|
8019
|
+
|
|
8020
|
+
app.get('/api/settings/deepseek', auth, (_req, res) => {
|
|
8021
|
+
if (!deepseekSettingsService) return res.status(501).json({ error: 'DeepSeek settings are unavailable' });
|
|
8022
|
+
return res.json(describeDeepSeekSettings());
|
|
8023
|
+
});
|
|
8024
|
+
|
|
8025
|
+
app.post('/api/settings/deepseek', auth, async (req, res) => {
|
|
8026
|
+
if (!deepseekSettingsService) return res.status(501).json({ error: 'DeepSeek settings are unavailable' });
|
|
8027
|
+
const parsed = parseDeepSeekSettingsUpdateRequest(req.body);
|
|
8028
|
+
if (!parsed.ok) return res.status(400).json({ error: parsed.error });
|
|
8029
|
+
// A starting balance a top-up has replaced is set anew when it is saved
|
|
8030
|
+
// again, the same amount too.
|
|
8031
|
+
const before = describeDeepSeekSettings();
|
|
8032
|
+
const startingBalanceReplaced = !!before.startingBalanceNote;
|
|
8033
|
+
const result = deepseekSettingsService.update(parsed, { startingBalanceReplaced });
|
|
8034
|
+
if (!result?.ok) {
|
|
8035
|
+
return res.status(result?.code === 'deepseek-key-removal-blocked' ? 409 : 400).json({
|
|
8036
|
+
error: result?.error || 'Failed to update DeepSeek settings',
|
|
8037
|
+
code: result?.code || null,
|
|
8038
|
+
activeConversationCount: Number(result?.activeConversationCount || 0),
|
|
8039
|
+
startedConversationCount: Number(result?.startedConversationCount || 0),
|
|
8040
|
+
activeQueueConversationCount: Number(result?.activeQueueConversationCount || 0),
|
|
8041
|
+
});
|
|
8042
|
+
}
|
|
8043
|
+
if (result.keyChanged) await stopLiveProviderWorkersForKeyRotation(DEEPSEEK_PROVIDER_TYPE);
|
|
8044
|
+
const discovery = result.discover
|
|
8045
|
+
? await deepseekSettingsService.refreshModels()
|
|
8046
|
+
: { ok: true, models: [], error: null };
|
|
8047
|
+
// A starting balance set anew: the balance is read right then and noted
|
|
8048
|
+
// as the latest read, so a later top-up is a rise from a balance read
|
|
8049
|
+
// after the setting, and one made before it is already in this read
|
|
8050
|
+
// (deepseekStartingBalanceState). A failed read changes nothing: the
|
|
8051
|
+
// setting then stays until a rise seen after a read that followed it.
|
|
8052
|
+
const setAt = result.settings?.startingBalanceSetAt || null;
|
|
8053
|
+
if (setAt && setAt !== before.startingBalanceSetAt) {
|
|
8054
|
+
try {
|
|
8055
|
+
const balance = await deepseekBalanceService?.getBalance?.({ force: true });
|
|
8056
|
+
planUsageService?.recordDeepSeekBalance?.(balance);
|
|
8057
|
+
} catch {}
|
|
8058
|
+
}
|
|
8059
|
+
// Removing the key moves unstarted DeepSeek conversations back to the
|
|
8060
|
+
// default provider, as for OpenAI and Cursor.
|
|
8061
|
+
const reconciliationResult = parsed.remove === true
|
|
8062
|
+
? await reconcileUnstartedConversationProviders({ enabled: false, model: DEFAULT_DEEPSEEK_MODEL, provider: DEEPSEEK_PROVIDER_TYPE })
|
|
8063
|
+
: { updatedUnstartedConversations: 0, skippedStartedConversations: 0, skippedActiveQueueConversations: 0, failedConversations: [] };
|
|
8064
|
+
const failedConversations = Array.isArray(reconciliationResult?.failedConversations) ? reconciliationResult.failedConversations : [];
|
|
8065
|
+
const settingsPayload = {
|
|
8066
|
+
...describeDeepSeekSettings(),
|
|
8067
|
+
reconciliation: {
|
|
8068
|
+
updatedUnstartedConversations: Number(reconciliationResult?.updatedUnstartedConversations || 0),
|
|
8069
|
+
skippedStartedConversations: Number(reconciliationResult?.skippedStartedConversations || 0),
|
|
8070
|
+
skippedActiveQueueConversations: Number(reconciliationResult?.skippedActiveQueueConversations || 0),
|
|
8071
|
+
failedConversations,
|
|
8072
|
+
},
|
|
8073
|
+
};
|
|
8074
|
+
io.emit('models_updated', buildModelCatalogWithProviders(getModelCatalogState()));
|
|
8075
|
+
io.emit('deepseek_settings_updated', settingsPayload);
|
|
8076
|
+
return res.json({
|
|
8077
|
+
ok: true,
|
|
8078
|
+
...settingsPayload,
|
|
8079
|
+
warning: failedConversations.length
|
|
8080
|
+
? `${failedConversations.length} unstarted conversation(s) could not switch provider.`
|
|
8081
|
+
: (discovery?.ok ? null : (discovery?.error || 'DeepSeek model discovery failed')),
|
|
8082
|
+
});
|
|
8083
|
+
});
|
|
8084
|
+
|
|
8085
|
+
// --- Claude API ----------------------------------------------------------
|
|
8086
|
+
// The key is written here and never read back: GET and every answer carry
|
|
8087
|
+
// `configured` only. A pasted Claude login token or Admin key is refused by
|
|
8088
|
+
// its prefix. A rotated key reaches a worker only at spawn, so the live
|
|
8089
|
+
// Claude API workers are stopped; the next message starts them again.
|
|
8090
|
+
app.get('/api/settings/claude-api', auth, (_req, res) => {
|
|
8091
|
+
if (!claudeApiSettingsService) return res.status(501).json({ error: 'Claude API settings are unavailable' });
|
|
8092
|
+
return res.json(claudeApiSettingsService.describe());
|
|
8093
|
+
});
|
|
8094
|
+
|
|
8095
|
+
app.post('/api/settings/claude-api', auth, async (req, res) => {
|
|
8096
|
+
if (!claudeApiSettingsService) return res.status(501).json({ error: 'Claude API settings are unavailable' });
|
|
8097
|
+
const parsed = parseClaudeApiSettingsUpdateRequest(req.body);
|
|
8098
|
+
if (!parsed.ok) return res.status(400).json({ error: parsed.error, code: parsed.code || null });
|
|
8099
|
+
const result = claudeApiSettingsService.update(parsed);
|
|
8100
|
+
if (!result?.ok) {
|
|
8101
|
+
return res.status(result?.code === 'claude-api-key-removal-blocked' ? 409 : 400).json({
|
|
8102
|
+
error: result?.error || 'Failed to update Claude API settings',
|
|
8103
|
+
code: result?.code || null,
|
|
8104
|
+
activeConversationCount: Number(result?.activeConversationCount || 0),
|
|
8105
|
+
startedConversationCount: Number(result?.startedConversationCount || 0),
|
|
8106
|
+
activeQueueConversationCount: Number(result?.activeQueueConversationCount || 0),
|
|
8107
|
+
});
|
|
8108
|
+
}
|
|
8109
|
+
if (result.keyChanged) await stopLiveProviderWorkersForKeyRotation(CLAUDE_API_PROVIDER_TYPE);
|
|
8110
|
+
const discovery = result.discover
|
|
8111
|
+
? await claudeApiSettingsService.refreshModels()
|
|
8112
|
+
: { ok: true, models: [], error: null };
|
|
8113
|
+
// Removing the key or switching the provider off moves unstarted Claude
|
|
8114
|
+
// API conversations back to the default provider, as for Claude itself.
|
|
8115
|
+
const reconciliationResult = result.removed === true || result.disabled === true
|
|
8116
|
+
? await reconcileUnstartedConversationProviders({ enabled: false, model: '', provider: CLAUDE_API_PROVIDER_TYPE })
|
|
8117
|
+
: { updatedUnstartedConversations: 0, skippedStartedConversations: 0, skippedActiveQueueConversations: 0, failedConversations: [] };
|
|
8118
|
+
const failedConversations = Array.isArray(reconciliationResult?.failedConversations) ? reconciliationResult.failedConversations : [];
|
|
8119
|
+
const settingsPayload = {
|
|
8120
|
+
...claudeApiSettingsService.describe(),
|
|
8121
|
+
reconciliation: {
|
|
8122
|
+
updatedUnstartedConversations: Number(reconciliationResult?.updatedUnstartedConversations || 0),
|
|
8123
|
+
skippedStartedConversations: Number(reconciliationResult?.skippedStartedConversations || 0),
|
|
8124
|
+
skippedActiveQueueConversations: Number(reconciliationResult?.skippedActiveQueueConversations || 0),
|
|
8125
|
+
failedConversations,
|
|
8126
|
+
},
|
|
8127
|
+
};
|
|
8128
|
+
io.emit('models_updated', buildModelCatalogWithProviders(getModelCatalogState()));
|
|
8129
|
+
io.emit('claude_api_settings_updated', settingsPayload);
|
|
8130
|
+
try { claudeApiStopLedger?.clearAll?.(); } catch {}
|
|
8131
|
+
try { checkClaudeApiCap('settings'); } catch {}
|
|
8132
|
+
// The key is saved whatever discovery said; Anthropic's refusal (a 401,
|
|
8133
|
+
// a key that needs a workspace) is the tab's status until a discovery
|
|
8134
|
+
// succeeds. A list dropped because the key changed meanwhile is no verdict.
|
|
8135
|
+
// A default the fallback list could not keep is named with the refusal.
|
|
8136
|
+
const previousModel = String(result.previousModel || '').trim();
|
|
8137
|
+
const defaultModelChanged = previousModel && settingsPayload.model && previousModel !== settingsPayload.model
|
|
8138
|
+
? { from: previousModel, to: settingsPayload.model }
|
|
8139
|
+
: null;
|
|
8140
|
+
const discoveryError = result.discover && discovery?.ok === false && discovery?.stale !== true
|
|
8141
|
+
? { error: discovery.error || 'Claude API model discovery failed', code: discovery.code || null, defaultModelChanged }
|
|
8142
|
+
: null;
|
|
8143
|
+
return res.json({
|
|
8144
|
+
ok: true,
|
|
8145
|
+
...settingsPayload,
|
|
8146
|
+
discoveryError,
|
|
8147
|
+
warning: failedConversations.length
|
|
8148
|
+
? `${failedConversations.length} unstarted conversation(s) could not switch provider.`
|
|
8149
|
+
: (discovery?.ok ? null : (discovery?.error || 'Claude API model discovery failed')),
|
|
8150
|
+
});
|
|
8151
|
+
});
|
|
8152
|
+
|
|
7629
8153
|
app.get('/api/settings/turn-ceiling', auth, (_req, res) => {
|
|
7630
8154
|
res.json({
|
|
7631
8155
|
ceilingMinutes: getTurnCeilingMinutes(),
|
|
@@ -8193,6 +8717,8 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8193
8717
|
const claudeSettings = getClaudeProviderSettings();
|
|
8194
8718
|
const cursorSettings = getCursorProviderSettings();
|
|
8195
8719
|
const grokSettings = getGrokProviderSettings();
|
|
8720
|
+
const deepseekSettings = getDeepSeekProviderSettings();
|
|
8721
|
+
const claudeApiSettings = getClaudeApiProviderSettings();
|
|
8196
8722
|
return {
|
|
8197
8723
|
...buildModelVariantCatalogPayload({
|
|
8198
8724
|
rows,
|
|
@@ -8222,6 +8748,23 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8222
8748
|
enabledModels: Array.isArray(grokSettings.models) ? grokSettings.models : [],
|
|
8223
8749
|
}
|
|
8224
8750
|
: null,
|
|
8751
|
+
deepseekModels: deepseekSettings?.enabled === true
|
|
8752
|
+
? {
|
|
8753
|
+
defaultModel: deepseekSettings.model,
|
|
8754
|
+
availableModels: Array.isArray(deepseekSettings.availableModels) ? deepseekSettings.availableModels : [],
|
|
8755
|
+
enabledModels: Array.isArray(deepseekSettings.models) ? deepseekSettings.models : [],
|
|
8756
|
+
}
|
|
8757
|
+
: null,
|
|
8758
|
+
// The Select Models → Claude API tab: its own list, "[1m]" ids included
|
|
8759
|
+
// (the tab shows them as on the Claude tab).
|
|
8760
|
+
claudeApiModels: claudeApiSettings?.enabled === true
|
|
8761
|
+
? {
|
|
8762
|
+
defaultModel: claudeApiSettings.model,
|
|
8763
|
+
availableModels: Array.isArray(claudeApiSettings.availableModels) ? claudeApiSettings.availableModels : [],
|
|
8764
|
+
enabledModels: Array.isArray(claudeApiSettings.models) ? claudeApiSettings.models : [],
|
|
8765
|
+
discovered: claudeApiSettings.discovered === true,
|
|
8766
|
+
}
|
|
8767
|
+
: null,
|
|
8225
8768
|
};
|
|
8226
8769
|
}
|
|
8227
8770
|
|
|
@@ -8240,6 +8783,8 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8240
8783
|
const claudeSettings = getClaudeProviderSettings();
|
|
8241
8784
|
const cursorSettings = getCursorProviderSettings();
|
|
8242
8785
|
const grokSettings = getGrokProviderSettings();
|
|
8786
|
+
const deepseekSettings = getDeepSeekProviderSettings();
|
|
8787
|
+
const claudeApiSettings = getClaudeApiProviderSettings();
|
|
8243
8788
|
const refreshTasks = [
|
|
8244
8789
|
refreshModelVariantCatalogFromCli(),
|
|
8245
8790
|
openAISettings?.enabled === true
|
|
@@ -8260,8 +8805,16 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8260
8805
|
typeof copilotModelDiscoveryService?.refresh === 'function'
|
|
8261
8806
|
? copilotModelDiscoveryService.refresh('manual-refresh')
|
|
8262
8807
|
: Promise.resolve({ ok: true, skipped: true, models: [], error: null }),
|
|
8808
|
+
deepseekSettings?.enabled === true && deepseekSettingsService
|
|
8809
|
+
? deepseekSettingsService.refreshModels()
|
|
8810
|
+
: Promise.resolve({ ok: true, skipped: true, models: [], error: null }),
|
|
8811
|
+
claudeApiSettings?.enabled === true && claudeApiSettingsService
|
|
8812
|
+
? claudeApiSettingsService.refreshModels()
|
|
8813
|
+
: Promise.resolve({ ok: true, skipped: true, models: [], error: null }),
|
|
8263
8814
|
];
|
|
8264
|
-
const [
|
|
8815
|
+
const [
|
|
8816
|
+
cliRefresh, openAIRefresh, claudeRefresh, cursorRefresh, grokRefresh, copilotDiscoveryRefresh, deepseekRefresh, claudeApiRefresh,
|
|
8817
|
+
] = await Promise.allSettled(refreshTasks);
|
|
8265
8818
|
if (cliRefresh.status === 'rejected') throw cliRefresh.reason;
|
|
8266
8819
|
const openAIModelDiscovery = openAIRefresh.status === 'fulfilled'
|
|
8267
8820
|
? openAIRefresh.value
|
|
@@ -8291,6 +8844,20 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8291
8844
|
models: Array.isArray(grokSettings?.models) ? grokSettings.models : [],
|
|
8292
8845
|
error: grokRefresh.reason?.message || 'Grok model discovery failed',
|
|
8293
8846
|
};
|
|
8847
|
+
const deepseekModelDiscovery = deepseekRefresh.status === 'fulfilled'
|
|
8848
|
+
? deepseekRefresh.value
|
|
8849
|
+
: {
|
|
8850
|
+
ok: false,
|
|
8851
|
+
models: Array.isArray(deepseekSettings?.models) ? deepseekSettings.models : [],
|
|
8852
|
+
error: deepseekRefresh.reason?.message || 'DeepSeek model discovery failed',
|
|
8853
|
+
};
|
|
8854
|
+
const claudeApiModelDiscovery = claudeApiRefresh.status === 'fulfilled'
|
|
8855
|
+
? claudeApiRefresh.value
|
|
8856
|
+
: {
|
|
8857
|
+
ok: false,
|
|
8858
|
+
models: Array.isArray(claudeApiSettings?.models) ? claudeApiSettings.models : [],
|
|
8859
|
+
error: claudeApiRefresh.reason?.message || 'Claude API model discovery failed',
|
|
8860
|
+
};
|
|
8294
8861
|
const copilotModelDiscovery = copilotDiscoveryRefresh.status === 'fulfilled'
|
|
8295
8862
|
? copilotDiscoveryRefresh.value
|
|
8296
8863
|
: { ok: false, models: [], error: copilotDiscoveryRefresh.reason?.message || 'Copilot model discovery failed' };
|
|
@@ -8304,6 +8871,8 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8304
8871
|
claudeModelDiscovery,
|
|
8305
8872
|
cursorModelDiscovery,
|
|
8306
8873
|
grokModelDiscovery,
|
|
8874
|
+
deepseekModelDiscovery,
|
|
8875
|
+
claudeApiModelDiscovery,
|
|
8307
8876
|
copilotModelDiscovery,
|
|
8308
8877
|
});
|
|
8309
8878
|
} catch (error) {
|
|
@@ -8351,6 +8920,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8351
8920
|
// stay in models[] (a selection must not vanish) but pickers grey them.
|
|
8352
8921
|
unavailableModels: Array.isArray(modelState.unavailableModels) ? modelState.unavailableModels : [],
|
|
8353
8922
|
claudeContextTiersByModel: modelState.claudeContextTiersByModel || {},
|
|
8923
|
+
claudeApiContextTiersByModel: modelState.claudeApiContextTiersByModel || {},
|
|
8354
8924
|
providersByModel: modelState.providersByModel || {},
|
|
8355
8925
|
stale: modelState.stale,
|
|
8356
8926
|
metadataValid: modelState.metadataValid === true,
|
|
@@ -8399,8 +8969,10 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8399
8969
|
copilotBilling: null, // { value, at }
|
|
8400
8970
|
grokBilling: null,
|
|
8401
8971
|
cursorBilling: null,
|
|
8972
|
+
deepseekBalance: null,
|
|
8402
8973
|
};
|
|
8403
|
-
|
|
8974
|
+
// `claude-api`'s card is read from the relay's own books on every answer.
|
|
8975
|
+
const USAGE_PROVIDER_IDS = ['github', 'claude', 'claude-cloud', 'cursor', 'grok', 'deepseek', 'claude-api'];
|
|
8404
8976
|
|
|
8405
8977
|
function parseUsageProviders(raw) {
|
|
8406
8978
|
const text = Array.isArray(raw) ? raw.join(',') : String(raw ?? '');
|
|
@@ -8455,6 +9027,16 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8455
9027
|
const claudeSettings = getClaudeProviderSettings();
|
|
8456
9028
|
const cursorSettings = getCursorProviderSettings();
|
|
8457
9029
|
const grokSettings = getGrokProviderSettings();
|
|
9030
|
+
const deepseekSettings = getDeepSeekProviderSettings();
|
|
9031
|
+
const deepseekEnabled = deepseekSettings?.enabled === true;
|
|
9032
|
+
// An unreadable export is no reason to lose the card: the estimate stays.
|
|
9033
|
+
const readDeepSeekActuals = () => {
|
|
9034
|
+
try {
|
|
9035
|
+
return deepseekUsageImportService?.readActuals?.() ?? null;
|
|
9036
|
+
} catch {
|
|
9037
|
+
return null;
|
|
9038
|
+
}
|
|
9039
|
+
};
|
|
8458
9040
|
let claudeCloudEnabled = false;
|
|
8459
9041
|
try {
|
|
8460
9042
|
claudeCloudEnabled = claudeCloudSettingsService?.getSettings?.()?.enabled === true;
|
|
@@ -8476,7 +9058,7 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8476
9058
|
: Promise.resolve(null);
|
|
8477
9059
|
|
|
8478
9060
|
// All reads side by side: the modal waits for the slowest, not the sum.
|
|
8479
|
-
const [copilot, billingEntry, grokEntry, cursorEntry, claudeAccount] = await Promise.all([
|
|
9061
|
+
const [copilot, billingEntry, grokEntry, cursorEntry, deepseekEntry, claudeAccount] = await Promise.all([
|
|
8480
9062
|
readCopilotSummary(),
|
|
8481
9063
|
cachedOrLive('copilotBilling', live('github'), () => (
|
|
8482
9064
|
typeof fetchCopilotBillingUsage === 'function'
|
|
@@ -8489,6 +9071,12 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8489
9071
|
cachedOrLive('cursorBilling', live('cursor'), () => (
|
|
8490
9072
|
cursorSettings?.enabled === true && typeof fetchCursorDashboardUsage === 'function' ? fetchCursorDashboardUsage() : null
|
|
8491
9073
|
)),
|
|
9074
|
+
// The service keeps its own minute of cache and answers a failure as
|
|
9075
|
+
// `{ ok: false, … }`, so a failed read replaces the earlier answer here
|
|
9076
|
+
// (which it carries as `previous`).
|
|
9077
|
+
cachedOrLive('deepseekBalance', live('deepseek'), () => (
|
|
9078
|
+
deepseekEnabled && typeof deepseekBalanceService?.getBalance === 'function' ? deepseekBalanceService.getBalance() : null
|
|
9079
|
+
)),
|
|
8492
9080
|
claudeAccountPromise,
|
|
8493
9081
|
]);
|
|
8494
9082
|
|
|
@@ -8503,6 +9091,14 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8503
9091
|
}
|
|
8504
9092
|
}
|
|
8505
9093
|
|
|
9094
|
+
// Built from the database on every answer, so it is always live.
|
|
9095
|
+
let claudeApiCard = null;
|
|
9096
|
+
try {
|
|
9097
|
+
claudeApiCard = claudeApiPlanUsage?.buildCard?.() || null;
|
|
9098
|
+
} catch {
|
|
9099
|
+
claudeApiCard = null;
|
|
9100
|
+
}
|
|
9101
|
+
|
|
8506
9102
|
const report = planUsageService.buildReport({
|
|
8507
9103
|
copilotSummary: copilot.summary,
|
|
8508
9104
|
copilotError: copilot.error,
|
|
@@ -8521,6 +9117,15 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8521
9117
|
grokConfigured: grokSettings?.enabled === true,
|
|
8522
9118
|
grokAllowances: getGrokPlanAllowanceSettings(),
|
|
8523
9119
|
grokBilling: grokEntry.value,
|
|
9120
|
+
deepseekConfigured: deepseekEnabled,
|
|
9121
|
+
deepseekBalance: deepseekEnabled ? deepseekEntry.value : null,
|
|
9122
|
+
deepseekMonthlyBudget: deepseekEnabled ? (deepseekSettings?.monthlyBudget ?? null) : null,
|
|
9123
|
+
deepseekMonthlyBudgetCurrency: deepseekEnabled ? (deepseekSettings?.monthlyBudgetCurrency ?? null) : null,
|
|
9124
|
+
deepseekStartingBalance: deepseekEnabled && deepseekSettings?.startingBalance
|
|
9125
|
+
? { amount: deepseekSettings.startingBalance, setAt: deepseekSettings.startingBalanceSetAt }
|
|
9126
|
+
: null,
|
|
9127
|
+
deepseekActuals: deepseekEnabled ? readDeepSeekActuals() : null,
|
|
9128
|
+
claudeApiCard,
|
|
8524
9129
|
});
|
|
8525
9130
|
|
|
8526
9131
|
// Legacy top-level fields stay alongside `providers` so an older cached
|
|
@@ -8535,6 +9140,8 @@ export function registerSessionsRoutes(app, deps) {
|
|
|
8535
9140
|
'claude-cloud': claudeAccount?.fetchedAt || null,
|
|
8536
9141
|
cursor: cursorEntry.at,
|
|
8537
9142
|
grok: grokEntry.at,
|
|
9143
|
+
deepseek: deepseekEntry.value?.fetchedAt || deepseekEntry.at,
|
|
9144
|
+
'claude-api': nowIso(),
|
|
8538
9145
|
},
|
|
8539
9146
|
});
|
|
8540
9147
|
});
|