@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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// POST /api/deepseek-usage-import: the two CSV files of DeepSeek's usage
|
|
4
|
+
// export, as text, from Settings → Providers → DeepSeek. The relay's user
|
|
5
|
+
// only: another relay's agent gets a 403. The import itself is in
|
|
6
|
+
// services/deepseek-usage-import.mjs.
|
|
7
|
+
|
|
8
|
+
import { readRemoteRelayRequest } from '../services/remote-relay-inbound.mjs';
|
|
9
|
+
|
|
10
|
+
// The two files of a month come to well under a megabyte.
|
|
11
|
+
export const DEEPSEEK_USAGE_IMPORT_MAX_BYTES = 2 * 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
// How long the import waits for the usage card's currency (the balance's).
|
|
14
|
+
export const DEEPSEEK_USAGE_IMPORT_CURRENCY_WAIT_MS = 5000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} deps
|
|
18
|
+
* @param {Function} [deps.getCardCurrency] the usage card's currency ('USD',
|
|
19
|
+
* 'CNY'), from the balance; may be async. The summary then says whether
|
|
20
|
+
* the card can use the export's rows.
|
|
21
|
+
* @param {Function} [deps.logError] where a failed import is logged
|
|
22
|
+
*/
|
|
23
|
+
export function registerDeepSeekUsageImportRoutes(app, deps) {
|
|
24
|
+
const {
|
|
25
|
+
auth,
|
|
26
|
+
deepseekUsageImportService,
|
|
27
|
+
remoteRelayInbound,
|
|
28
|
+
getCardCurrency = null,
|
|
29
|
+
logError = (message, err) => console.error(message, err),
|
|
30
|
+
} = deps;
|
|
31
|
+
|
|
32
|
+
async function cardCurrency() {
|
|
33
|
+
if (typeof getCardCurrency !== 'function') return null;
|
|
34
|
+
let timer = null;
|
|
35
|
+
try {
|
|
36
|
+
return await Promise.race([
|
|
37
|
+
Promise.resolve().then(() => getCardCurrency()),
|
|
38
|
+
new Promise((resolve) => { timer = setTimeout(() => resolve(null), DEEPSEEK_USAGE_IMPORT_CURRENCY_WAIT_MS); }),
|
|
39
|
+
]);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
} finally {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
app.post('/api/deepseek-usage-import', auth, async (req, res) => {
|
|
48
|
+
if (readRemoteRelayRequest(req, remoteRelayInbound || {}).remote) {
|
|
49
|
+
return res.status(403).json({ error: 'The DeepSeek usage export is imported by this relay\'s user only.' });
|
|
50
|
+
}
|
|
51
|
+
if (typeof deepseekUsageImportService?.importExport !== 'function') {
|
|
52
|
+
return res.status(501).json({ error: 'The DeepSeek usage import is not available on this relay.' });
|
|
53
|
+
}
|
|
54
|
+
const amount = req.body?.amount;
|
|
55
|
+
const cost = req.body?.cost;
|
|
56
|
+
if (typeof amount !== 'string' || typeof cost !== 'string') {
|
|
57
|
+
return res.status(400).json({ error: 'Choose both files of the export: the amount file and the cost file.' });
|
|
58
|
+
}
|
|
59
|
+
if (Buffer.byteLength(amount, 'utf8') + Buffer.byteLength(cost, 'utf8') > DEEPSEEK_USAGE_IMPORT_MAX_BYTES) {
|
|
60
|
+
return res.status(413).json({ error: 'The files are larger than 2 MB together; export a shorter period.' });
|
|
61
|
+
}
|
|
62
|
+
let result;
|
|
63
|
+
try {
|
|
64
|
+
result = deepseekUsageImportService.importExport({ amount, cost, cardCurrency: await cardCurrency() });
|
|
65
|
+
} catch (err) {
|
|
66
|
+
logError('[deepseek-usage-import] the import failed:', err);
|
|
67
|
+
return res.status(500).json({ error: 'The import failed on the relay; nothing was imported. Its log has the reason.' });
|
|
68
|
+
}
|
|
69
|
+
if (!result?.ok) return res.status(400).json({ error: result?.error || 'The export could not be read.' });
|
|
70
|
+
res.json({ ok: true, summary: result.summary });
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -38,6 +38,7 @@ import { claudePlanUsageFromResult, normalizeClaudePlanUsage } from '../services
|
|
|
38
38
|
import { isUsageLimitTerminalError } from '../services/usage-limit-pause-service.mjs';
|
|
39
39
|
import { normalizeCopilotWorkerUsage } from '../services/plan-usage-copilot.mjs';
|
|
40
40
|
import { normalizeGrokTurnUsage } from '../services/plan-usage-grok.mjs';
|
|
41
|
+
import { normalizeDeepSeekUsageReport } from '../services/plan-usage-deepseek.mjs';
|
|
41
42
|
import { openAIReasoningEffortsForModel } from '../../shared/openai-reasoning.mjs';
|
|
42
43
|
import { DEFAULT_CLAUDE_REASONING_EFFORTS } from '../services/provider-reasoning-effort.mjs';
|
|
43
44
|
import { claudeBaseModelId, claudeLongContextModelId } from '../../shared/model-id.mjs';
|
|
@@ -71,7 +72,15 @@ import { createStaleAttemptLogLimiter } from '../services/stale-attempt-log-limi
|
|
|
71
72
|
import { createSubagentTimelinePublisher } from '../services/subagent-viewer-rooms.mjs';
|
|
72
73
|
import { createLocalRequestCheck } from '../services/request-boundary.mjs';
|
|
73
74
|
import { buildClaudeCloudDelivery, isClaudeCloudProviderType } from '../services/claude-cloud-session-service.mjs';
|
|
75
|
+
import {
|
|
76
|
+
DEEPSEEK_REASONING_EFFORTS,
|
|
77
|
+
DEFAULT_DEEPSEEK_REASONING_EFFORT,
|
|
78
|
+
deepseekModelAcceptsImages,
|
|
79
|
+
deepseekModelLabel,
|
|
80
|
+
deepseekReasoningEffortFor,
|
|
81
|
+
} from '../../shared/deepseek-models.mjs';
|
|
74
82
|
import { TASK_OUTPUT_NONE_MESSAGES } from '../public/app/task-console-render.mjs';
|
|
83
|
+
import { recordClaudeCliStart } from '../services/claude-session-leftovers.mjs';
|
|
75
84
|
import {
|
|
76
85
|
admitRemoteRelayRequest,
|
|
77
86
|
readRemoteRelayRequest,
|
|
@@ -151,6 +160,7 @@ function normalizeRequestedProviderType(value = '') {
|
|
|
151
160
|
if (normalized === 'claude' || normalized === 'anthropic') return 'claude';
|
|
152
161
|
// Its own provider, never read as `claude`.
|
|
153
162
|
if (normalized === 'claude-cloud') return 'claude-cloud';
|
|
163
|
+
if (normalized === 'deepseek') return 'deepseek';
|
|
154
164
|
return '';
|
|
155
165
|
}
|
|
156
166
|
|
|
@@ -159,6 +169,7 @@ const BOOTSTRAP_ONLY_PROVIDER_LABELS = Object.freeze({
|
|
|
159
169
|
grok: 'Grok',
|
|
160
170
|
claude: 'Claude',
|
|
161
171
|
'claude-cloud': 'Claude Cloud',
|
|
172
|
+
deepseek: 'DeepSeek',
|
|
162
173
|
});
|
|
163
174
|
|
|
164
175
|
function deriveModelOrigin(model) {
|
|
@@ -1832,6 +1843,7 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
1832
1843
|
getCursorProviderSettings = () => ({ enabled: false, model: '', models: [] }),
|
|
1833
1844
|
getGrokProviderSettings = () => ({ enabled: false, model: '', models: [] }),
|
|
1834
1845
|
getClaudeCloudProviderSettings = () => ({ enabled: false, defaultModel: '', environmentId: '', models: [] }),
|
|
1846
|
+
getDeepSeekProviderSettings = () => ({ enabled: false, model: '', models: [] }),
|
|
1835
1847
|
rebindUnstartedOpenAIConversationModel = null,
|
|
1836
1848
|
normalizeRelayMode,
|
|
1837
1849
|
DEFAULT_RELAY_MODE,
|
|
@@ -1891,6 +1903,10 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
1891
1903
|
sessionActivityTracker = null,
|
|
1892
1904
|
// A worker's SDK session id or a conversation id → the conversation row.
|
|
1893
1905
|
resolveConversationRecord = null,
|
|
1906
|
+
// DeepSeek's Claude Code config directory, which the relay owns: each CLI
|
|
1907
|
+
// start a DeepSeek worker reports is recorded there for the delete
|
|
1908
|
+
// (claude-session-leftovers.mjs).
|
|
1909
|
+
deepseekClaudeConfigDir = '',
|
|
1894
1910
|
} = deps;
|
|
1895
1911
|
|
|
1896
1912
|
const ttyConsoleActive = runtimeState?.ttyConsoleActive === true;
|
|
@@ -1983,6 +1999,22 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
1983
1999
|
if (/^Tool failed:/i.test(value)) return { kind: 'tool_result', text: value, metadata: { isError: true } };
|
|
1984
2000
|
return { kind: 'note', text: value };
|
|
1985
2001
|
}
|
|
2002
|
+
// Whether a conversation's history holds an image the user attached. Read
|
|
2003
|
+
// only for a DeepSeek model switch; prepared on first use, so a database
|
|
2004
|
+
// without the messages table (tests) is answered with "no".
|
|
2005
|
+
let conversationHasImageStatement = null;
|
|
2006
|
+
function conversationHasImageAttachment(conversationId) {
|
|
2007
|
+
try {
|
|
2008
|
+
conversationHasImageStatement ||= db.prepare(`
|
|
2009
|
+
SELECT 1 FROM messages
|
|
2010
|
+
WHERE conversation_id = ? AND role = 'user' AND attachments LIKE '%"image/%'
|
|
2011
|
+
LIMIT 1
|
|
2012
|
+
`);
|
|
2013
|
+
return !!conversationHasImageStatement.get(conversationId);
|
|
2014
|
+
} catch {
|
|
2015
|
+
return false;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
1986
2018
|
const getMessageAttachmentsByConversation = db.prepare(`
|
|
1987
2019
|
SELECT attachments
|
|
1988
2020
|
FROM messages
|
|
@@ -4549,7 +4581,8 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
4549
4581
|
|| runtimeProviderType === 'claude'
|
|
4550
4582
|
|| runtimeProviderType === 'cursor'
|
|
4551
4583
|
|| runtimeProviderType === 'grok'
|
|
4552
|
-
|| runtimeProviderType === 'claude-cloud'
|
|
4584
|
+
|| runtimeProviderType === 'claude-cloud'
|
|
4585
|
+
|| runtimeProviderType === 'deepseek';
|
|
4553
4586
|
const configuredOpenAIModel = String(configuredOpenAI?.model || '').trim();
|
|
4554
4587
|
const availableOpenAIModels = new Set(
|
|
4555
4588
|
(Array.isArray(configuredOpenAI?.models) ? configuredOpenAI.models : [])
|
|
@@ -4673,6 +4706,32 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
4673
4706
|
});
|
|
4674
4707
|
}
|
|
4675
4708
|
}
|
|
4709
|
+
// DeepSeek conversations are bootstrap-created only too. Its ids are its
|
|
4710
|
+
// own ("deepseek-*"), so an id alone may name it for an unbound session.
|
|
4711
|
+
const runtimeUsesDeepSeek = runtimeProviderType === 'deepseek';
|
|
4712
|
+
if (!shouldCreateConversation && !runtimeUsesDeepSeek) {
|
|
4713
|
+
let requestTargetsDeepSeek = requestedProviderType === 'deepseek';
|
|
4714
|
+
if (
|
|
4715
|
+
!requestTargetsDeepSeek
|
|
4716
|
+
&& !runtimeBoundToExplicitProvider
|
|
4717
|
+
&& requestedOpenAIModel
|
|
4718
|
+
&& !requestedModelAvailableInGitHub
|
|
4719
|
+
) {
|
|
4720
|
+
const deepseekSettings = getDeepSeekProviderSettings();
|
|
4721
|
+
const deepseekOnlyModels = new Set([
|
|
4722
|
+
String(deepseekSettings?.model || '').trim(),
|
|
4723
|
+
...(Array.isArray(deepseekSettings?.models) ? deepseekSettings.models : [])
|
|
4724
|
+
.map((value) => String(value || '').trim()),
|
|
4725
|
+
].filter(Boolean));
|
|
4726
|
+
requestTargetsDeepSeek = deepseekSettings?.enabled === true && deepseekOnlyModels.has(requestedOpenAIModel);
|
|
4727
|
+
}
|
|
4728
|
+
if (requestTargetsDeepSeek) {
|
|
4729
|
+
return res.status(409).json({
|
|
4730
|
+
error: 'DeepSeek model selection requires creating a new DeepSeek conversation',
|
|
4731
|
+
code: 'DEEPSEEK_MODEL_REQUIRES_NEW_CONVERSATION',
|
|
4732
|
+
});
|
|
4733
|
+
}
|
|
4734
|
+
}
|
|
4676
4735
|
// Claude Cloud conversations are bootstrap-created only as well: each
|
|
4677
4736
|
// carries the repository its cloud session clones. Only an explicit
|
|
4678
4737
|
// provider counts here — the cloud's model ids are the Claude provider's
|
|
@@ -4842,6 +4901,45 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
4842
4901
|
// sends set_model before the message), so a changed model is taken as
|
|
4843
4902
|
// for a local Claude chat.
|
|
4844
4903
|
const claudeCloudModel = claudeCloudModelSelection ? claudeCloudModelSelection.model : '';
|
|
4904
|
+
// DeepSeek conversations resolve models against the DeepSeek catalog and
|
|
4905
|
+
// may switch between turns, like Claude ones (the worker's CLI switches
|
|
4906
|
+
// before the next message). A model DeepSeek does not offer is refused.
|
|
4907
|
+
const configuredDeepSeek = runtimeUsesDeepSeek ? getDeepSeekProviderSettings() : null;
|
|
4908
|
+
const pinnedDeepSeekModel = runtimeUsesDeepSeek ? String(existingRuntimeSession?.provider_model || '').trim() : '';
|
|
4909
|
+
const deepseekModelSelection = runtimeUsesDeepSeek
|
|
4910
|
+
? resolveProviderModelSelection({
|
|
4911
|
+
requestedModel: String(model || '').trim(),
|
|
4912
|
+
configuredModel: pinnedDeepSeekModel || String(configuredDeepSeek?.model || '').trim(),
|
|
4913
|
+
availableModels: [
|
|
4914
|
+
String(configuredDeepSeek?.model || '').trim(),
|
|
4915
|
+
pinnedDeepSeekModel,
|
|
4916
|
+
...(Array.isArray(configuredDeepSeek?.models) ? configuredDeepSeek.models : []),
|
|
4917
|
+
].map((value) => String(value || '').trim()).filter(Boolean),
|
|
4918
|
+
})
|
|
4919
|
+
: null;
|
|
4920
|
+
if (deepseekModelSelection && !deepseekModelSelection.ok) {
|
|
4921
|
+
return res.status(400).json({
|
|
4922
|
+
error: `DeepSeek model "${deepseekModelSelection.requestedModel}" is not available`,
|
|
4923
|
+
code: 'DEEPSEEK_MODEL_UNAVAILABLE',
|
|
4924
|
+
supportedModels: deepseekModelSelection.availableModels || [],
|
|
4925
|
+
});
|
|
4926
|
+
}
|
|
4927
|
+
const deepseekModel = deepseekModelSelection ? deepseekModelSelection.model : '';
|
|
4928
|
+
// A model without image input cannot take over a session whose transcript
|
|
4929
|
+
// carries images: the resumed history would fail the whole request.
|
|
4930
|
+
if (
|
|
4931
|
+
runtimeUsesDeepSeek
|
|
4932
|
+
&& deepseekModel
|
|
4933
|
+
&& pinnedDeepSeekModel
|
|
4934
|
+
&& deepseekModel.toLowerCase() !== pinnedDeepSeekModel.toLowerCase()
|
|
4935
|
+
&& !deepseekModelAcceptsImages(deepseekModel)
|
|
4936
|
+
&& conversationHasImageAttachment(conversationId)
|
|
4937
|
+
) {
|
|
4938
|
+
return res.status(409).json({
|
|
4939
|
+
error: `${deepseekModelLabel(deepseekModel)} takes no images, and this conversation already has some. Stay on ${deepseekModelLabel(pinnedDeepSeekModel)}, or start a new DeepSeek conversation for ${deepseekModelLabel(deepseekModel)}.`,
|
|
4940
|
+
code: 'DEEPSEEK_MODEL_TEXT_ONLY',
|
|
4941
|
+
});
|
|
4942
|
+
}
|
|
4845
4943
|
// What the request itself chose. Only these are saved as the
|
|
4846
4944
|
// conversation's preference; a value filled in below never is.
|
|
4847
4945
|
const explicitModelGiven = !!requestedOpenAIModel;
|
|
@@ -4910,7 +5008,15 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
4910
5008
|
reasoningEffort: null,
|
|
4911
5009
|
error: claudeCloudModel ? null : 'Claude Cloud model is not configured',
|
|
4912
5010
|
}
|
|
4913
|
-
:
|
|
5011
|
+
: (runtimeUsesDeepSeek
|
|
5012
|
+
? {
|
|
5013
|
+
ok: !!deepseekModel,
|
|
5014
|
+
model: deepseekModel,
|
|
5015
|
+
modelVariantId: deepseekModel,
|
|
5016
|
+
reasoningEffort: null,
|
|
5017
|
+
error: deepseekModel ? null : 'DeepSeek model is not configured',
|
|
5018
|
+
}
|
|
5019
|
+
: githubModelResolution)))));
|
|
4914
5020
|
if (!modelResolution.ok) return res.status(400).json({ error: modelResolution.error, supportedModels: modelResolution.available || [] });
|
|
4915
5021
|
const requestedModel = String(modelResolution.model || '').trim();
|
|
4916
5022
|
const requestedAutoModel = requestedModel.toLowerCase() === AUTO_MODEL_SENTINEL;
|
|
@@ -4987,6 +5093,16 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
4987
5093
|
const effort = supported.includes(requestedEffort) ? requestedEffort : 'none';
|
|
4988
5094
|
return { ok: true, effort, supported };
|
|
4989
5095
|
};
|
|
5096
|
+
// DeepSeek takes none/low/high/max for every model; other names map to
|
|
5097
|
+
// what DeepSeek would run ("medium" is high), and no stated effort is the
|
|
5098
|
+
// conversation's own, else high, DeepSeek's default.
|
|
5099
|
+
const resolveDeepSeekReasoningEffort = () => {
|
|
5100
|
+
const supported = [...DEEPSEEK_REASONING_EFFORTS];
|
|
5101
|
+
const requestedEffort = deepseekReasoningEffortFor(explicitReasoningEffort);
|
|
5102
|
+
if (requestedEffort) return { ok: true, effort: requestedEffort, supported };
|
|
5103
|
+
const kept = deepseekReasoningEffortFor(conversationReasoningEffort);
|
|
5104
|
+
return { ok: true, effort: kept || DEFAULT_DEEPSEEK_REASONING_EFFORT, supported };
|
|
5105
|
+
};
|
|
4990
5106
|
let reasoningResolution = useOpenAIProvider
|
|
4991
5107
|
? resolveOpenAIReasoningEffort(
|
|
4992
5108
|
explicitReasoningEffort || conversationEffortFor(openAIReasoningEffortsForModel(openAIModel)) || '',
|
|
@@ -5001,10 +5117,12 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
5001
5117
|
// A cloud session takes no effort: whatever was asked, it is none.
|
|
5002
5118
|
: (runtimeUsesClaudeCloud
|
|
5003
5119
|
? { ok: true, effort: 'none', supported: ['none'] }
|
|
5004
|
-
:
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5120
|
+
: (runtimeUsesDeepSeek
|
|
5121
|
+
? resolveDeepSeekReasoningEffort()
|
|
5122
|
+
: resolveRequestedReasoningEffort(
|
|
5123
|
+
requestedModel,
|
|
5124
|
+
explicitReasoningEffort || modelResolution.reasoningEffort || null,
|
|
5125
|
+
))))));
|
|
5008
5126
|
// No effort sent (or the model's variant carried one it lacks): the
|
|
5009
5127
|
// conversation's own effort, clamped to the model's levels, else its
|
|
5010
5128
|
// lowest level that is not 'none'. Some models list 'none' and the API
|
|
@@ -5040,8 +5158,9 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
5040
5158
|
}
|
|
5041
5159
|
} else if (!['default', 'long_context'].includes(requestedContextTier)) {
|
|
5042
5160
|
return res.status(400).json({ error: 'Unsupported context tier' });
|
|
5043
|
-
} else if (runtimeUsesClaudeCloud) {
|
|
5044
|
-
// The cloud has no long-context tier to pick
|
|
5161
|
+
} else if (runtimeUsesClaudeCloud || runtimeUsesDeepSeek) {
|
|
5162
|
+
// The cloud has no long-context tier to pick, and DeepSeek's one window
|
|
5163
|
+
// is already 1M.
|
|
5045
5164
|
normalizedContextTier = 'default';
|
|
5046
5165
|
}
|
|
5047
5166
|
if (!requestedRelayMode) return res.status(400).json({ error: 'Unsupported relay mode' });
|
|
@@ -5277,6 +5396,17 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
5277
5396
|
) {
|
|
5278
5397
|
stmts.updateRuntimeSessionProvider.run('claude', claudeModel, claudeModel, now, runtimeSession.id);
|
|
5279
5398
|
}
|
|
5399
|
+
// DeepSeek follows the composer the same way: a relaunched worker reads
|
|
5400
|
+
// provider_model, so it has to be the model last used.
|
|
5401
|
+
if (
|
|
5402
|
+
runtimeUsesDeepSeek
|
|
5403
|
+
&& deepseekModel
|
|
5404
|
+
&& runtimeSession?.id
|
|
5405
|
+
&& deepseekModel !== String(existingRuntimeSession?.provider_model || '').trim()
|
|
5406
|
+
&& typeof stmts.updateRuntimeSessionProvider?.run === 'function'
|
|
5407
|
+
) {
|
|
5408
|
+
stmts.updateRuntimeSessionProvider.run('deepseek', deepseekModel, deepseekModel, now, runtimeSession.id);
|
|
5409
|
+
}
|
|
5280
5410
|
// A cloud conversation follows the composer like a Claude one: the
|
|
5281
5411
|
// worker switches the sandbox's model before the message.
|
|
5282
5412
|
if (
|
|
@@ -5784,7 +5914,10 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
5784
5914
|
});
|
|
5785
5915
|
|
|
5786
5916
|
// POST /api/claude-native-session — Claude worker persists the native Agent
|
|
5787
|
-
// SDK session id so later turns can resume it across worker restarts.
|
|
5917
|
+
// SDK session id so later turns can resume it across worker restarts. A
|
|
5918
|
+
// DeepSeek worker also reports each CLI start (`cliStartedAt`, ms), which
|
|
5919
|
+
// the relay records so deleting the conversation finds every CLI's shell
|
|
5920
|
+
// snapshot.
|
|
5788
5921
|
app.post('/api/claude-native-session', auth, (req, res) => {
|
|
5789
5922
|
touchCli();
|
|
5790
5923
|
const conversationId = String(req.body?.conversationId || '').trim();
|
|
@@ -5796,7 +5929,10 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
5796
5929
|
if (!runtimeSession) {
|
|
5797
5930
|
return res.status(404).json({ error: 'Runtime session not found for conversation' });
|
|
5798
5931
|
}
|
|
5799
|
-
|
|
5932
|
+
// The DeepSeek worker runs the same CLI (against DeepSeek) and resumes
|
|
5933
|
+
// its sessions the same way.
|
|
5934
|
+
const nativeSessionProvider = String(runtimeSession.provider_type || 'github').trim().toLowerCase();
|
|
5935
|
+
if (nativeSessionProvider !== 'claude' && nativeSessionProvider !== 'deepseek') {
|
|
5800
5936
|
return res.status(409).json({ error: 'Conversation is not bound to the Claude provider' });
|
|
5801
5937
|
}
|
|
5802
5938
|
if (typeof stmts.updateRuntimeSessionClaudeNativeSessionId?.run !== 'function') {
|
|
@@ -5807,6 +5943,10 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
5807
5943
|
new Date().toISOString(),
|
|
5808
5944
|
conversationId,
|
|
5809
5945
|
);
|
|
5946
|
+
const cliStartedAt = Number(req.body?.cliStartedAt);
|
|
5947
|
+
if (nativeSessionProvider === 'deepseek' && Number.isFinite(cliStartedAt) && cliStartedAt > 0 && cliStartedAt <= Date.now() + 60_000) {
|
|
5948
|
+
recordClaudeCliStart({ configRoot: deepseekClaudeConfigDir, nativeSessionId: claudeNativeSessionId, startedAt: cliStartedAt });
|
|
5949
|
+
}
|
|
5810
5950
|
res.json({ ok: true });
|
|
5811
5951
|
});
|
|
5812
5952
|
|
|
@@ -5834,9 +5974,9 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
5834
5974
|
return res.status(404).json({ error: 'Runtime session not found for conversation' });
|
|
5835
5975
|
}
|
|
5836
5976
|
// The Claude Cloud worker reports the same shape (the cloud session's
|
|
5837
|
-
// context and token usage), so
|
|
5977
|
+
// context and token usage), and so does the DeepSeek one (the same CLI).
|
|
5838
5978
|
const contextUsageProvider = String(runtimeSession.provider_type || 'github').trim().toLowerCase();
|
|
5839
|
-
if (contextUsageProvider !== 'claude' && contextUsageProvider !== 'claude-cloud') {
|
|
5979
|
+
if (contextUsageProvider !== 'claude' && contextUsageProvider !== 'claude-cloud' && contextUsageProvider !== 'deepseek') {
|
|
5840
5980
|
return res.status(409).json({ error: 'Conversation is not bound to the Claude provider' });
|
|
5841
5981
|
}
|
|
5842
5982
|
if (typeof stmts.updateRuntimeSessionContextUsage?.run !== 'function') {
|
|
@@ -5883,6 +6023,40 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
5883
6023
|
res.json({ ok: true, cycle: applied.cycle.key });
|
|
5884
6024
|
});
|
|
5885
6025
|
|
|
6026
|
+
// POST /api/deepseek-plan-usage — DeepSeek worker reports, after a turn,
|
|
6027
|
+
// the CLI's running token totals per model (cache hit, cache miss, output)
|
|
6028
|
+
// for its Claude Code session, with the time and the id of the CLI
|
|
6029
|
+
// process (`runId`). The relay books what they add to the totals last
|
|
6030
|
+
// reported for that session (a resumed CLI starts from its session's
|
|
6031
|
+
// earlier totals), with the estimated cost at the price of that time
|
|
6032
|
+
// (shared/deepseek-pricing.mjs). Totals below the checkpoint under the
|
|
6033
|
+
// checkpoint's run id are a stale report (retried or out of order) and
|
|
6034
|
+
// book nothing; under another run id, or without one, they are a CLI
|
|
6035
|
+
// counting from zero again, booked in full. The web searches and fetches
|
|
6036
|
+
// come as `requests`: with a run id the process's running counts, booked
|
|
6037
|
+
// like the tokens; without one the turn's counts, booked as they are.
|
|
6038
|
+
// Priced from 2026-10-31 16:00 UTC.
|
|
6039
|
+
app.post('/api/deepseek-plan-usage', auth, (req, res) => {
|
|
6040
|
+
touchCli();
|
|
6041
|
+
if (!planUsageService?.recordDeepSeekUsageReport) {
|
|
6042
|
+
return res.status(500).json({ error: 'Plan usage storage is unavailable' });
|
|
6043
|
+
}
|
|
6044
|
+
const conversationId = String(req.body?.conversationId || '').trim();
|
|
6045
|
+
if (!conversationId) return res.status(400).json({ error: 'Missing conversationId' });
|
|
6046
|
+
const runtimeSession = stmts.getRuntimeSessionByConversation.get(conversationId);
|
|
6047
|
+
if (!runtimeSession) {
|
|
6048
|
+
return res.status(404).json({ error: 'Runtime session not found for conversation' });
|
|
6049
|
+
}
|
|
6050
|
+
if (String(runtimeSession.provider_type || 'github').trim().toLowerCase() !== 'deepseek') {
|
|
6051
|
+
return res.status(409).json({ error: 'Conversation is not bound to the DeepSeek provider' });
|
|
6052
|
+
}
|
|
6053
|
+
const parsed = normalizeDeepSeekUsageReport(req.body);
|
|
6054
|
+
if (!parsed.ok) return res.status(400).json({ error: parsed.error });
|
|
6055
|
+
const booked = planUsageService.recordDeepSeekUsageReport({ ...parsed.report, conversationId });
|
|
6056
|
+
if (!booked) return res.status(400).json({ error: 'Missing usable usage metrics' });
|
|
6057
|
+
res.json({ ok: true, at: booked.at, models: booked.models.map((entry) => entry.model) });
|
|
6058
|
+
});
|
|
6059
|
+
|
|
5886
6060
|
// POST /api/claude-plan-usage — Claude worker reports the session's
|
|
5887
6061
|
// structured /usage data (plan rate-limit windows + session cost totals).
|
|
5888
6062
|
//
|
|
@@ -6795,7 +6969,10 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
6795
6969
|
body: JSON.stringify({
|
|
6796
6970
|
messageId: operation.queue_message_id,
|
|
6797
6971
|
conversationId: operation.conversation_id,
|
|
6798
|
-
|
|
6972
|
+
// `terminalError` is the field /api/response reads; under any other
|
|
6973
|
+
// name the failure was refused as an empty reply and the row stayed
|
|
6974
|
+
// processing until the relay recovered and re-ran it.
|
|
6975
|
+
terminalError: {
|
|
6799
6976
|
code: 'IMAGE_ATTEMPT_INTERRUPTED',
|
|
6800
6977
|
message: 'Image generation may have completed before the relay was interrupted. It was not retried to avoid duplicate images.',
|
|
6801
6978
|
retryable: false,
|
|
@@ -6956,7 +7133,7 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
6956
7133
|
body: JSON.stringify({
|
|
6957
7134
|
messageId: operation.queue_message_id,
|
|
6958
7135
|
conversationId: operation.conversation_id,
|
|
6959
|
-
|
|
7136
|
+
terminalError: failure,
|
|
6960
7137
|
}),
|
|
6961
7138
|
}).catch(() => null);
|
|
6962
7139
|
if (!failResponse?.ok) {
|
|
@@ -8031,7 +8208,7 @@ export function registerMessagesRoutes(app, deps) {
|
|
|
8031
8208
|
// The providers whose workers name a shell task's output (a file, a spool
|
|
8032
8209
|
// or a snapshot). A task of theirs without one comes from a worker that
|
|
8033
8210
|
// started before the console existed.
|
|
8034
|
-
const TASK_OUTPUT_PROVIDERS = new Set(['claude', 'github', 'copilot', 'grok']);
|
|
8211
|
+
const TASK_OUTPUT_PROVIDERS = new Set(['claude', 'github', 'copilot', 'grok', 'deepseek']);
|
|
8035
8212
|
const SUBAGENT_OUTPUT_MESSAGE = TASK_OUTPUT_NONE_MESSAGES.subagent;
|
|
8036
8213
|
|
|
8037
8214
|
// Why a task has no output to show, for the console's `none` answer. The
|