@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
|
@@ -12,10 +12,19 @@
|
|
|
12
12
|
* has to be *derived*, so two small bookkeeping tables are kept — a cumulative
|
|
13
13
|
* per-agent checkpoint (to diff against) and per-cycle pool totals (the
|
|
14
14
|
* accumulated result). Neither grows with conversation history.
|
|
15
|
+
*
|
|
16
|
+
* DeepSeek's estimated spend is summed per model (migration 0024): one row
|
|
17
|
+
* per model. Its workers report the CLI's running totals, so a checkpoint per
|
|
18
|
+
* (conversation, CLI session, model) is kept to diff against, as for Cursor.
|
|
19
|
+
* The same sums go into a row per UTC day and model (migration 0025), for
|
|
20
|
+
* the card's daily chart and its monthly budget; that table grows by at most
|
|
21
|
+
* one row per model per day.
|
|
15
22
|
*/
|
|
16
23
|
|
|
17
24
|
import {
|
|
18
25
|
PLAN_USAGE_VERSION,
|
|
26
|
+
toFiniteNumber,
|
|
27
|
+
toIsoTimestamp,
|
|
19
28
|
toTrimmedString,
|
|
20
29
|
} from './plan-usage-contract.mjs';
|
|
21
30
|
import { buildCopilotPlanCard } from './plan-usage-copilot.mjs';
|
|
@@ -35,6 +44,35 @@ import {
|
|
|
35
44
|
normalizeGrokTurnUsage,
|
|
36
45
|
resolveGrokBillingCycle,
|
|
37
46
|
} from './plan-usage-grok.mjs';
|
|
47
|
+
import { buildDeepSeekPlanCard } from './plan-usage-deepseek.mjs';
|
|
48
|
+
import {
|
|
49
|
+
DEEPSEEK_USAGE_CHECKPOINTS_TABLE,
|
|
50
|
+
DEEPSEEK_USAGE_TOTALS_TABLE,
|
|
51
|
+
migrateDeepSeekUsageTotals,
|
|
52
|
+
} from '../migrations/0024-deepseek-usage-totals.mjs';
|
|
53
|
+
import {
|
|
54
|
+
DEEPSEEK_BALANCE_REFERENCE_TABLE,
|
|
55
|
+
DEEPSEEK_USAGE_DAYS_TABLE,
|
|
56
|
+
migrateDeepSeekUsageRequestsDaily,
|
|
57
|
+
} from '../migrations/0025-deepseek-usage-requests-daily.mjs';
|
|
58
|
+
import { migrateDeepSeekUsageServedModels } from '../migrations/0026-deepseek-usage-served-models.mjs';
|
|
59
|
+
import { migrateDeepSeekUsageCheckpointRuns } from '../migrations/0027-deepseek-usage-checkpoint-runs.mjs';
|
|
60
|
+
import { migrateDeepSeekUsageCheckpointRequestRuns } from '../migrations/0028-deepseek-usage-checkpoint-request-runs.mjs';
|
|
61
|
+
import { migrateDeepSeekBalanceRises } from '../migrations/0029-deepseek-balance-rises.mjs';
|
|
62
|
+
import { deepseekServedModel } from '../../shared/deepseek-models.mjs';
|
|
63
|
+
import { estimateCost, estimateSearchCost } from '../../shared/deepseek-pricing.mjs';
|
|
64
|
+
|
|
65
|
+
// A DeepSeek report's token fields and their checkpoint columns.
|
|
66
|
+
const DEEPSEEK_TOKEN_FIELDS = Object.freeze([
|
|
67
|
+
['hitTokens', 'hit_tokens'],
|
|
68
|
+
['missTokens', 'miss_tokens'],
|
|
69
|
+
['outputTokens', 'output_tokens'],
|
|
70
|
+
]);
|
|
71
|
+
// Its request fields (web searches and fetches) and their checkpoint columns.
|
|
72
|
+
const DEEPSEEK_REQUEST_FIELDS = Object.freeze([
|
|
73
|
+
['searchRequests', 'search_requests'],
|
|
74
|
+
['fetchRequests', 'fetch_requests'],
|
|
75
|
+
]);
|
|
38
76
|
|
|
39
77
|
export const PROVIDER_USAGE_SNAPSHOT_TABLE = 'provider_usage_snapshots';
|
|
40
78
|
export const CURSOR_USAGE_CHECKPOINT_TABLE = 'cursor_usage_checkpoints';
|
|
@@ -103,9 +141,29 @@ const CHECKPOINT_COLUMNS = [
|
|
|
103
141
|
export function ensurePlanUsageSchema(db) {
|
|
104
142
|
if (typeof db?.exec !== 'function') return false;
|
|
105
143
|
db.exec(PLAN_USAGE_SCHEMA_SQL);
|
|
144
|
+
// The boot runs the same migration (db-schema.mjs); a database the service
|
|
145
|
+
// is handed on its own (tests, tools) gets the table here.
|
|
146
|
+
migrateDeepSeekUsageTotals(db);
|
|
147
|
+
migrateDeepSeekUsageRequestsDaily(db);
|
|
148
|
+
migrateDeepSeekUsageServedModels(db);
|
|
149
|
+
migrateDeepSeekUsageCheckpointRuns(db);
|
|
150
|
+
migrateDeepSeekUsageCheckpointRequestRuns(db);
|
|
151
|
+
migrateDeepSeekBalanceRises(db);
|
|
106
152
|
return true;
|
|
107
153
|
}
|
|
108
154
|
|
|
155
|
+
/**
|
|
156
|
+
* The model a DeepSeek report's entry is booked under: the DeepSeek model
|
|
157
|
+
* that served it. An older worker names a sub-agent's model as Claude Code
|
|
158
|
+
* requested it (`claude-opus-5-5`, which DeepSeek serves with V4 Pro); the
|
|
159
|
+
* table and the prices know DeepSeek's ids only. A name DeepSeek is not known
|
|
160
|
+
* to serve is booked as it is, without a price.
|
|
161
|
+
*/
|
|
162
|
+
function deepseekBookedModel(value) {
|
|
163
|
+
const model = toTrimmedString(value);
|
|
164
|
+
return deepseekServedModel(model) || model;
|
|
165
|
+
}
|
|
166
|
+
|
|
109
167
|
function rowToCheckpoint(row) {
|
|
110
168
|
if (!row) return null;
|
|
111
169
|
const checkpoint = { agentId: row.agent_id, capturedAt: row.updated_at };
|
|
@@ -201,6 +259,88 @@ export function createPlanUsageService({ db, now = () => new Date(), dbg = () =>
|
|
|
201
259
|
updated_at = excluded.updated_at
|
|
202
260
|
`),
|
|
203
261
|
clearGrokCycle: db.prepare(`DELETE FROM ${GROK_USAGE_CYCLE_TABLE} WHERE cycle_key = ?`),
|
|
262
|
+
// A cost of NULL is "no estimate": it stays NULL until a turn of the
|
|
263
|
+
// model can be priced, and never turns a known sum into NULL. The tools
|
|
264
|
+
// cost (searches and fetches) is always known.
|
|
265
|
+
addDeepSeekTotals: db.prepare(`
|
|
266
|
+
INSERT INTO ${DEEPSEEK_USAGE_TOTALS_TABLE} (
|
|
267
|
+
model, hit_tokens, miss_tokens, output_tokens, search_requests, fetch_requests, turn_count,
|
|
268
|
+
cost_usd, cost_cny, tools_cost_usd, tools_cost_cny, first_seen_at, last_seen_at, updated_at
|
|
269
|
+
) VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?)
|
|
270
|
+
ON CONFLICT(model) DO UPDATE SET
|
|
271
|
+
hit_tokens = ${DEEPSEEK_USAGE_TOTALS_TABLE}.hit_tokens + excluded.hit_tokens,
|
|
272
|
+
miss_tokens = ${DEEPSEEK_USAGE_TOTALS_TABLE}.miss_tokens + excluded.miss_tokens,
|
|
273
|
+
output_tokens = ${DEEPSEEK_USAGE_TOTALS_TABLE}.output_tokens + excluded.output_tokens,
|
|
274
|
+
search_requests = ${DEEPSEEK_USAGE_TOTALS_TABLE}.search_requests + excluded.search_requests,
|
|
275
|
+
fetch_requests = ${DEEPSEEK_USAGE_TOTALS_TABLE}.fetch_requests + excluded.fetch_requests,
|
|
276
|
+
turn_count = ${DEEPSEEK_USAGE_TOTALS_TABLE}.turn_count + 1,
|
|
277
|
+
cost_usd = CASE WHEN excluded.cost_usd IS NULL THEN ${DEEPSEEK_USAGE_TOTALS_TABLE}.cost_usd
|
|
278
|
+
ELSE COALESCE(${DEEPSEEK_USAGE_TOTALS_TABLE}.cost_usd, 0) + excluded.cost_usd END,
|
|
279
|
+
cost_cny = CASE WHEN excluded.cost_cny IS NULL THEN ${DEEPSEEK_USAGE_TOTALS_TABLE}.cost_cny
|
|
280
|
+
ELSE COALESCE(${DEEPSEEK_USAGE_TOTALS_TABLE}.cost_cny, 0) + excluded.cost_cny END,
|
|
281
|
+
tools_cost_usd = ${DEEPSEEK_USAGE_TOTALS_TABLE}.tools_cost_usd + excluded.tools_cost_usd,
|
|
282
|
+
tools_cost_cny = ${DEEPSEEK_USAGE_TOTALS_TABLE}.tools_cost_cny + excluded.tools_cost_cny,
|
|
283
|
+
first_seen_at = MIN(${DEEPSEEK_USAGE_TOTALS_TABLE}.first_seen_at, excluded.first_seen_at),
|
|
284
|
+
last_seen_at = MAX(${DEEPSEEK_USAGE_TOTALS_TABLE}.last_seen_at, excluded.last_seen_at),
|
|
285
|
+
updated_at = excluded.updated_at
|
|
286
|
+
`),
|
|
287
|
+
// The same sums per UTC day, for the daily chart and the month's spend.
|
|
288
|
+
addDeepSeekDay: db.prepare(`
|
|
289
|
+
INSERT INTO ${DEEPSEEK_USAGE_DAYS_TABLE} (
|
|
290
|
+
day, model, hit_tokens, miss_tokens, output_tokens, search_requests, fetch_requests, turn_count,
|
|
291
|
+
cost_usd, cost_cny, tools_cost_usd, tools_cost_cny, updated_at
|
|
292
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?)
|
|
293
|
+
ON CONFLICT(day, model) DO UPDATE SET
|
|
294
|
+
hit_tokens = ${DEEPSEEK_USAGE_DAYS_TABLE}.hit_tokens + excluded.hit_tokens,
|
|
295
|
+
miss_tokens = ${DEEPSEEK_USAGE_DAYS_TABLE}.miss_tokens + excluded.miss_tokens,
|
|
296
|
+
output_tokens = ${DEEPSEEK_USAGE_DAYS_TABLE}.output_tokens + excluded.output_tokens,
|
|
297
|
+
search_requests = ${DEEPSEEK_USAGE_DAYS_TABLE}.search_requests + excluded.search_requests,
|
|
298
|
+
fetch_requests = ${DEEPSEEK_USAGE_DAYS_TABLE}.fetch_requests + excluded.fetch_requests,
|
|
299
|
+
turn_count = ${DEEPSEEK_USAGE_DAYS_TABLE}.turn_count + 1,
|
|
300
|
+
cost_usd = CASE WHEN excluded.cost_usd IS NULL THEN ${DEEPSEEK_USAGE_DAYS_TABLE}.cost_usd
|
|
301
|
+
ELSE COALESCE(${DEEPSEEK_USAGE_DAYS_TABLE}.cost_usd, 0) + excluded.cost_usd END,
|
|
302
|
+
cost_cny = CASE WHEN excluded.cost_cny IS NULL THEN ${DEEPSEEK_USAGE_DAYS_TABLE}.cost_cny
|
|
303
|
+
ELSE COALESCE(${DEEPSEEK_USAGE_DAYS_TABLE}.cost_cny, 0) + excluded.cost_cny END,
|
|
304
|
+
tools_cost_usd = ${DEEPSEEK_USAGE_DAYS_TABLE}.tools_cost_usd + excluded.tools_cost_usd,
|
|
305
|
+
tools_cost_cny = ${DEEPSEEK_USAGE_DAYS_TABLE}.tools_cost_cny + excluded.tools_cost_cny,
|
|
306
|
+
updated_at = excluded.updated_at
|
|
307
|
+
`),
|
|
308
|
+
listDeepSeekTotals: db.prepare(`SELECT * FROM ${DEEPSEEK_USAGE_TOTALS_TABLE} ORDER BY last_seen_at DESC, model`),
|
|
309
|
+
listDeepSeekDays: db.prepare(`SELECT * FROM ${DEEPSEEK_USAGE_DAYS_TABLE} WHERE day >= ? ORDER BY day, model`),
|
|
310
|
+
firstDeepSeekDay: db.prepare(`SELECT MIN(day) AS day FROM ${DEEPSEEK_USAGE_DAYS_TABLE}`),
|
|
311
|
+
listDeepSeekBalanceReferences: db.prepare(`SELECT * FROM ${DEEPSEEK_BALANCE_REFERENCE_TABLE} ORDER BY currency`),
|
|
312
|
+
getDeepSeekBalanceReference: db.prepare(`SELECT * FROM ${DEEPSEEK_BALANCE_REFERENCE_TABLE} WHERE currency = ?`),
|
|
313
|
+
upsertDeepSeekBalanceReference: db.prepare(`
|
|
314
|
+
INSERT INTO ${DEEPSEEK_BALANCE_REFERENCE_TABLE} (currency, reference_total, last_total, reference_at, updated_at, rose_at, last_read_at, rose_from_at)
|
|
315
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
316
|
+
ON CONFLICT(currency) DO UPDATE SET
|
|
317
|
+
reference_total = excluded.reference_total,
|
|
318
|
+
last_total = excluded.last_total,
|
|
319
|
+
reference_at = excluded.reference_at,
|
|
320
|
+
updated_at = excluded.updated_at,
|
|
321
|
+
rose_at = excluded.rose_at,
|
|
322
|
+
last_read_at = excluded.last_read_at,
|
|
323
|
+
rose_from_at = excluded.rose_from_at
|
|
324
|
+
`),
|
|
325
|
+
getDeepSeekCheckpoint: db.prepare(`
|
|
326
|
+
SELECT * FROM ${DEEPSEEK_USAGE_CHECKPOINTS_TABLE}
|
|
327
|
+
WHERE conversation_id = ? AND native_session_id = ? AND model = ?
|
|
328
|
+
`),
|
|
329
|
+
upsertDeepSeekCheckpoint: db.prepare(`
|
|
330
|
+
INSERT INTO ${DEEPSEEK_USAGE_CHECKPOINTS_TABLE} (
|
|
331
|
+
conversation_id, native_session_id, model, run_id, request_run_id,
|
|
332
|
+
hit_tokens, miss_tokens, output_tokens, search_requests, fetch_requests, updated_at
|
|
333
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
334
|
+
ON CONFLICT(conversation_id, native_session_id, model) DO UPDATE SET
|
|
335
|
+
run_id = excluded.run_id,
|
|
336
|
+
request_run_id = excluded.request_run_id,
|
|
337
|
+
hit_tokens = excluded.hit_tokens,
|
|
338
|
+
miss_tokens = excluded.miss_tokens,
|
|
339
|
+
output_tokens = excluded.output_tokens,
|
|
340
|
+
search_requests = excluded.search_requests,
|
|
341
|
+
fetch_requests = excluded.fetch_requests,
|
|
342
|
+
updated_at = excluded.updated_at
|
|
343
|
+
`),
|
|
204
344
|
};
|
|
205
345
|
|
|
206
346
|
function nowIso() {
|
|
@@ -384,6 +524,252 @@ export function createPlanUsageService({ db, now = () => new Date(), dbg = () =>
|
|
|
384
524
|
return { cycle };
|
|
385
525
|
}
|
|
386
526
|
|
|
527
|
+
/**
|
|
528
|
+
* Book one DeepSeek report (normalizeDeepSeekUsageReport): a worker's
|
|
529
|
+
* running token totals per model for one CLI session, with its estimate
|
|
530
|
+
* at the price of the report's time in both currencies. The checkpoint
|
|
531
|
+
* per (conversation, session, model) holds the totals last booked and the
|
|
532
|
+
* `runId` they came from, the id the worker gives each CLI process.
|
|
533
|
+
*
|
|
534
|
+
* With a run id the totals are that process's own, counted from zero when
|
|
535
|
+
* it started (the worker takes the CLI's session totals apart, so what a
|
|
536
|
+
* resumed CLI restored from its transcript is never in them). Under the
|
|
537
|
+
* run the checkpoint came from only the increase is booked, field by
|
|
538
|
+
* field, and a total below the checkpoint is a stale report (a post
|
|
539
|
+
* retried or delivered out of order): it books nothing and leaves the
|
|
540
|
+
* checkpoint at its high-water mark. Under another run the report is a
|
|
541
|
+
* new process, booked in full, from which the checkpoint starts over; a
|
|
542
|
+
* late report of the earlier process after that is booked in full too,
|
|
543
|
+
* since the relay cannot tell it from a process counting from zero.
|
|
544
|
+
*
|
|
545
|
+
* Without a run id (an older worker) the totals are the CLI's own, which
|
|
546
|
+
* a resumed CLI continues from its session's earlier ones: what they add
|
|
547
|
+
* is booked, and a drop is a CLI counting from zero again, booked in full.
|
|
548
|
+
*
|
|
549
|
+
* Web searches and fetches: a report with a run id carries the process's
|
|
550
|
+
* running counts per model, booked like the tokens under a run id of their
|
|
551
|
+
* own (`request_run_id`: an entry with counts only, as a sub-agent's model
|
|
552
|
+
* that searched without tokens of its own, leaves the token run alone).
|
|
553
|
+
* Without a run id they are the turn's counts, booked as they come.
|
|
554
|
+
* Returns what was booked: models with nothing new are left out.
|
|
555
|
+
*/
|
|
556
|
+
function recordDeepSeekUsageReport(report) {
|
|
557
|
+
const at = toTrimmedString(report?.at);
|
|
558
|
+
const conversationId = toTrimmedString(report?.conversationId);
|
|
559
|
+
const nativeSessionId = toTrimmedString(report?.nativeSessionId);
|
|
560
|
+
const runId = toTrimmedString(report?.runId) || '';
|
|
561
|
+
const totals = Array.isArray(report?.totals) ? report.totals : [];
|
|
562
|
+
const requests = Array.isArray(report?.requests) ? report.requests : [];
|
|
563
|
+
if (!at || !conversationId || !nativeSessionId || (!totals.length && !requests.length)) return null;
|
|
564
|
+
const timestamp = nowIso();
|
|
565
|
+
const day = at.slice(0, 10);
|
|
566
|
+
// Entries that name one DeepSeek model in two ways (its own id and a
|
|
567
|
+
// Claude id it served) are summed, like the worker sums the CLI's names.
|
|
568
|
+
const totalsByModel = new Map();
|
|
569
|
+
for (const entry of totals) {
|
|
570
|
+
const model = deepseekBookedModel(entry?.model);
|
|
571
|
+
if (!model) continue;
|
|
572
|
+
const current = totalsByModel.get(model) || { hitTokens: 0, missTokens: 0, outputTokens: 0 };
|
|
573
|
+
for (const [field] of DEEPSEEK_TOKEN_FIELDS) current[field] += Math.max(0, Number(entry?.[field]) || 0);
|
|
574
|
+
totalsByModel.set(model, current);
|
|
575
|
+
}
|
|
576
|
+
const requestsByModel = new Map();
|
|
577
|
+
for (const entry of requests) {
|
|
578
|
+
const model = deepseekBookedModel(entry?.model);
|
|
579
|
+
if (!model) continue;
|
|
580
|
+
const current = requestsByModel.get(model) || { searchRequests: 0, fetchRequests: 0 };
|
|
581
|
+
current.searchRequests += Math.max(0, Number(entry.searchRequests) || 0);
|
|
582
|
+
current.fetchRequests += Math.max(0, Number(entry.fetchRequests) || 0);
|
|
583
|
+
requestsByModel.set(model, current);
|
|
584
|
+
}
|
|
585
|
+
const models = [...new Set([...totalsByModel.keys(), ...requestsByModel.keys()])];
|
|
586
|
+
const booked = [];
|
|
587
|
+
const persist = db.transaction(() => {
|
|
588
|
+
for (const model of models) {
|
|
589
|
+
const entry = totalsByModel.get(model);
|
|
590
|
+
const modelRequests = requestsByModel.get(model);
|
|
591
|
+
const delta = { model, hitTokens: 0, missTokens: 0, outputTokens: 0, searchRequests: 0, fetchRequests: 0 };
|
|
592
|
+
const previous = stmts.getDeepSeekCheckpoint.get(conversationId, nativeSessionId, model) || null;
|
|
593
|
+
// The checkpoint's totals (counts) came from this very process: they
|
|
594
|
+
// can only have grown since.
|
|
595
|
+
const sameRun = Boolean(runId) && Boolean(previous) && String(previous.run_id || '') === runId;
|
|
596
|
+
const sameRequestRun = Boolean(runId) && Boolean(previous) && String(previous.request_run_id || '') === runId;
|
|
597
|
+
// What the checkpoint will hold: the report's totals, or the
|
|
598
|
+
// high-water mark when the report is stale.
|
|
599
|
+
const checkpoint = { runId: String(previous?.run_id || ''), requestRunId: String(previous?.request_run_id || '') };
|
|
600
|
+
for (const [field, column] of DEEPSEEK_TOKEN_FIELDS) checkpoint[field] = Number(previous?.[column]) || 0;
|
|
601
|
+
for (const [field, column] of DEEPSEEK_REQUEST_FIELDS) checkpoint[field] = Number(previous?.[column]) || 0;
|
|
602
|
+
if (entry) {
|
|
603
|
+
const reported = {};
|
|
604
|
+
for (const [field] of DEEPSEEK_TOKEN_FIELDS) reported[field] = Number(entry[field]) || 0;
|
|
605
|
+
const drop = DEEPSEEK_TOKEN_FIELDS.some(([field]) => reported[field] < checkpoint[field]);
|
|
606
|
+
if (drop && sameRun) {
|
|
607
|
+
// Stale: a retried post, or one that arrived after a later one.
|
|
608
|
+
} else {
|
|
609
|
+
// A process's own totals under a new run are all new spend; an
|
|
610
|
+
// older worker's session totals add their increase, or are a CLI
|
|
611
|
+
// counting from zero again when they dropped.
|
|
612
|
+
const full = runId ? !sameRun : drop;
|
|
613
|
+
for (const [field] of DEEPSEEK_TOKEN_FIELDS) {
|
|
614
|
+
delta[field] = full ? reported[field] : reported[field] - checkpoint[field];
|
|
615
|
+
checkpoint[field] = reported[field];
|
|
616
|
+
}
|
|
617
|
+
checkpoint.runId = runId;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (modelRequests && runId) {
|
|
621
|
+
// Running counts for this process: in full under a new run (the
|
|
622
|
+
// checkpoint's counts are another process's), the increase under
|
|
623
|
+
// the same one, nothing when they are at or below the checkpoint.
|
|
624
|
+
const drop = DEEPSEEK_REQUEST_FIELDS.some(([field]) => modelRequests[field] < checkpoint[field]);
|
|
625
|
+
if (!(drop && sameRequestRun)) {
|
|
626
|
+
for (const [field] of DEEPSEEK_REQUEST_FIELDS) {
|
|
627
|
+
delta[field] = sameRequestRun ? modelRequests[field] - checkpoint[field] : modelRequests[field];
|
|
628
|
+
checkpoint[field] = modelRequests[field];
|
|
629
|
+
}
|
|
630
|
+
checkpoint.requestRunId = runId;
|
|
631
|
+
}
|
|
632
|
+
} else if (modelRequests) {
|
|
633
|
+
// An older worker: the turn's counts, booked as they come.
|
|
634
|
+
delta.searchRequests = modelRequests.searchRequests;
|
|
635
|
+
delta.fetchRequests = modelRequests.fetchRequests;
|
|
636
|
+
}
|
|
637
|
+
if (entry || (modelRequests && runId)) {
|
|
638
|
+
stmts.upsertDeepSeekCheckpoint.run(
|
|
639
|
+
conversationId,
|
|
640
|
+
nativeSessionId,
|
|
641
|
+
model,
|
|
642
|
+
checkpoint.runId,
|
|
643
|
+
checkpoint.requestRunId,
|
|
644
|
+
checkpoint.hitTokens,
|
|
645
|
+
checkpoint.missTokens,
|
|
646
|
+
checkpoint.outputTokens,
|
|
647
|
+
checkpoint.searchRequests,
|
|
648
|
+
checkpoint.fetchRequests,
|
|
649
|
+
timestamp,
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
const tokenCount = delta.hitTokens + delta.missTokens + delta.outputTokens;
|
|
653
|
+
if (tokenCount <= 0 && delta.searchRequests + delta.fetchRequests <= 0) continue;
|
|
654
|
+
const tokens = { hitTokens: delta.hitTokens, missTokens: delta.missTokens, outputTokens: delta.outputTokens };
|
|
655
|
+
// No tokens booked (a request-only report) is no token estimate.
|
|
656
|
+
delta.costUsd = tokenCount > 0 ? estimateCost({ model, at, currency: 'USD', ...tokens }) : null;
|
|
657
|
+
delta.costCny = tokenCount > 0 ? estimateCost({ model, at, currency: 'CNY', ...tokens }) : null;
|
|
658
|
+
const requested = { at, searchRequests: delta.searchRequests, fetchRequests: delta.fetchRequests };
|
|
659
|
+
delta.toolsCostUsd = estimateSearchCost({ ...requested, currency: 'USD' }) || 0;
|
|
660
|
+
delta.toolsCostCny = estimateSearchCost({ ...requested, currency: 'CNY' }) || 0;
|
|
661
|
+
const values = [
|
|
662
|
+
delta.hitTokens,
|
|
663
|
+
delta.missTokens,
|
|
664
|
+
delta.outputTokens,
|
|
665
|
+
delta.searchRequests,
|
|
666
|
+
delta.fetchRequests,
|
|
667
|
+
delta.costUsd,
|
|
668
|
+
delta.costCny,
|
|
669
|
+
delta.toolsCostUsd,
|
|
670
|
+
delta.toolsCostCny,
|
|
671
|
+
];
|
|
672
|
+
stmts.addDeepSeekTotals.run(model, ...values, at, at, timestamp);
|
|
673
|
+
stmts.addDeepSeekDay.run(day, model, ...values, timestamp);
|
|
674
|
+
booked.push(delta);
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
persist();
|
|
678
|
+
return { at, models: booked };
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Ten decimals, as each estimate is kept: the sum's binary-float tail goes.
|
|
682
|
+
const tenDecimals = (value) => Math.round(Number(value) * 1e10) / 1e10;
|
|
683
|
+
const nullableMoney = (value) => (value === null || value === undefined ? null : tenDecimals(value));
|
|
684
|
+
|
|
685
|
+
function deepseekSumsOf(row) {
|
|
686
|
+
return {
|
|
687
|
+
model: row.model,
|
|
688
|
+
hitTokens: Number(row.hit_tokens) || 0,
|
|
689
|
+
missTokens: Number(row.miss_tokens) || 0,
|
|
690
|
+
outputTokens: Number(row.output_tokens) || 0,
|
|
691
|
+
searchRequests: Number(row.search_requests) || 0,
|
|
692
|
+
fetchRequests: Number(row.fetch_requests) || 0,
|
|
693
|
+
turnCount: Number(row.turn_count) || 0,
|
|
694
|
+
costUsd: nullableMoney(row.cost_usd),
|
|
695
|
+
costCny: nullableMoney(row.cost_cny),
|
|
696
|
+
toolsCostUsd: tenDecimals(row.tools_cost_usd || 0),
|
|
697
|
+
toolsCostCny: tenDecimals(row.tools_cost_cny || 0),
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function readDeepSeekUsageTotals() {
|
|
702
|
+
const models = stmts.listDeepSeekTotals.all().map((row) => ({
|
|
703
|
+
...deepseekSumsOf(row),
|
|
704
|
+
firstSeenAt: row.first_seen_at || null,
|
|
705
|
+
lastSeenAt: row.last_seen_at || null,
|
|
706
|
+
}));
|
|
707
|
+
return { models };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* The daily sums per model from `since` (a `YYYY-MM-DD` UTC day) on:
|
|
712
|
+
* `{ days: [{ day, model, ...sums }] }`, oldest first.
|
|
713
|
+
*/
|
|
714
|
+
function readDeepSeekUsageDays({ since } = {}) {
|
|
715
|
+
const from = /^\d{4}-\d{2}-\d{2}$/.test(String(since ?? '')) ? String(since) : '0000-00-00';
|
|
716
|
+
const days = stmts.listDeepSeekDays.all(from).map((row) => ({ day: row.day, ...deepseekSumsOf(row) }));
|
|
717
|
+
// The first day the daily table holds: the history starts with the
|
|
718
|
+
// relay that brought it, not with the first DeepSeek turn.
|
|
719
|
+
const first = stmts.firstDeepSeekDay.get()?.day || null;
|
|
720
|
+
return { days, firstDay: first };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function deepseekBalanceReferenceOf(row) {
|
|
724
|
+
return {
|
|
725
|
+
currency: String(row.currency),
|
|
726
|
+
referenceTotal: Number(row.reference_total) || 0,
|
|
727
|
+
lastTotal: Number(row.last_total) || 0,
|
|
728
|
+
referenceAt: row.reference_at || null,
|
|
729
|
+
// When the balance last rose after the first read, and when the
|
|
730
|
+
// balance before that rise was read (migration 0029).
|
|
731
|
+
roseAt: row.rose_at || null,
|
|
732
|
+
roseFromAt: row.rose_from_at || null,
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/** The balance references per currency (`recordDeepSeekBalance`). */
|
|
737
|
+
function readDeepSeekBalanceReferences() {
|
|
738
|
+
return stmts.listDeepSeekBalanceReferences.all().map(deepseekBalanceReferenceOf);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Notes a balance read (deepseek-balance-service.mjs's `{ ok: true,
|
|
743
|
+
* balances: [{ currency, total }] }`) for the balance meter, which shows
|
|
744
|
+
* what is left of the balance since the last top-up. Per currency the
|
|
745
|
+
* reference is the balance right after its latest rise (a top-up or a
|
|
746
|
+
* grant); before any rise was seen it is the highest balance seen, so the
|
|
747
|
+
* meter is full on the first read and fills with the spend. A failed read,
|
|
748
|
+
* or one repeated from the cache, changes nothing. Returns the references.
|
|
749
|
+
*/
|
|
750
|
+
function recordDeepSeekBalance(balance) {
|
|
751
|
+
if (balance?.ok !== true || !Array.isArray(balance.balances)) return readDeepSeekBalanceReferences();
|
|
752
|
+
const readAt = toIsoTimestamp(balance.fetchedAt) || nowIso();
|
|
753
|
+
const timestamp = nowIso();
|
|
754
|
+
db.transaction(() => {
|
|
755
|
+
for (const entry of balance.balances) {
|
|
756
|
+
const currency = toTrimmedString(entry?.currency)?.toUpperCase();
|
|
757
|
+
const total = toFiniteNumber(entry?.total);
|
|
758
|
+
if (!currency || total === null || total < 0) continue;
|
|
759
|
+
const row = stmts.getDeepSeekBalanceReference.get(currency);
|
|
760
|
+
const rose = row ? total > Number(row.last_total) : false;
|
|
761
|
+
const reference = row && !rose ? Math.max(Number(row.reference_total) || 0, total) : total;
|
|
762
|
+
const referenceAt = !row || rose || reference !== Number(row.reference_total) ? readAt : (row.reference_at || readAt);
|
|
763
|
+
const roseAt = rose ? readAt : (row?.rose_at || null);
|
|
764
|
+
// The read before the rise: a row from before last_read_at was
|
|
765
|
+
// recorded at about the time it was read.
|
|
766
|
+
const roseFromAt = rose ? (row.last_read_at || row.updated_at || null) : (row?.rose_from_at || null);
|
|
767
|
+
stmts.upsertDeepSeekBalanceReference.run(currency, reference, total, referenceAt, timestamp, roseAt, readAt, roseFromAt);
|
|
768
|
+
}
|
|
769
|
+
})();
|
|
770
|
+
return readDeepSeekBalanceReferences();
|
|
771
|
+
}
|
|
772
|
+
|
|
387
773
|
// Claude Cloud conversations keep their reported cost on the runtime
|
|
388
774
|
// session row (migration 0007). That table is the relay's, not this
|
|
389
775
|
// service's, so the statements are prepared on first use, and only once
|
|
@@ -464,6 +850,18 @@ export function createPlanUsageService({ db, now = () => new Date(), dbg = () =>
|
|
|
464
850
|
grokConfigured = false,
|
|
465
851
|
grokAllowances = null,
|
|
466
852
|
grokBilling = null,
|
|
853
|
+
deepseekConfigured = false,
|
|
854
|
+
// The balance service's answer (deepseek-balance-service.mjs), or null.
|
|
855
|
+
deepseekBalance = null,
|
|
856
|
+
// The DeepSeek tab's monthly budget and its currency ('USD' or 'CNY'; null
|
|
857
|
+
// for a budget saved without one, read in the balance's currency), or null.
|
|
858
|
+
deepseekMonthlyBudget = null,
|
|
859
|
+
deepseekMonthlyBudgetCurrency = null,
|
|
860
|
+
deepseekStartingBalance = null,
|
|
861
|
+
// The imported DeepSeek export's figures of the saved key
|
|
862
|
+
// (deepseek-usage-import.mjs readActuals()), or null: read only, laid
|
|
863
|
+
// over the estimate by the card.
|
|
864
|
+
deepseekActuals = null,
|
|
467
865
|
} = {}) {
|
|
468
866
|
const generatedAt = nowIso();
|
|
469
867
|
const claudeSnapshot = readSnapshot('claude');
|
|
@@ -544,6 +942,34 @@ export function createPlanUsageService({ db, now = () => new Date(), dbg = () =>
|
|
|
544
942
|
}));
|
|
545
943
|
}
|
|
546
944
|
|
|
945
|
+
// Opt-in like Grok: no card while DeepSeek is off.
|
|
946
|
+
if (deepseekConfigured === true) {
|
|
947
|
+
// The daily chart's 30 days and this calendar month, whichever reaches
|
|
948
|
+
// further back (UTC days, like the booking).
|
|
949
|
+
const today = generatedAt.slice(0, 10);
|
|
950
|
+
const monthStart = `${today.slice(0, 7)}-01`;
|
|
951
|
+
const thirtyDaysAgo = new Date(Date.parse(`${today}T00:00:00.000Z`) - 29 * 86_400_000).toISOString().slice(0, 10);
|
|
952
|
+
// The daily rows reach back to the export's first day too, so the card
|
|
953
|
+
// can take the estimate of every day the export covers out of the totals.
|
|
954
|
+
const firstActualDay = [
|
|
955
|
+
...(Array.isArray(deepseekActuals?.rows) ? deepseekActuals.rows : []),
|
|
956
|
+
...(Array.isArray(deepseekActuals?.coveredDays) ? deepseekActuals.coveredDays : []),
|
|
957
|
+
].map((row) => String(row?.day || '')).filter((value) => /^\d{4}-\d{2}-\d{2}$/.test(value)).sort()[0] || null;
|
|
958
|
+
const since = [thirtyDaysAgo, monthStart, firstActualDay].filter(Boolean).sort()[0];
|
|
959
|
+
providers.push(buildDeepSeekPlanCard({
|
|
960
|
+
balance: deepseekBalance,
|
|
961
|
+
balanceReferences: recordDeepSeekBalance(deepseekBalance),
|
|
962
|
+
totals: readDeepSeekUsageTotals(),
|
|
963
|
+
days: readDeepSeekUsageDays({ since }),
|
|
964
|
+
monthlyBudget: deepseekMonthlyBudget,
|
|
965
|
+
monthlyBudgetCurrency: deepseekMonthlyBudgetCurrency,
|
|
966
|
+
startingBalance: deepseekStartingBalance,
|
|
967
|
+
now: generatedAt,
|
|
968
|
+
configured: true,
|
|
969
|
+
actuals: deepseekActuals,
|
|
970
|
+
}));
|
|
971
|
+
}
|
|
972
|
+
|
|
547
973
|
return {
|
|
548
974
|
version: PLAN_USAGE_VERSION,
|
|
549
975
|
generatedAt,
|
|
@@ -560,6 +986,11 @@ export function createPlanUsageService({ db, now = () => new Date(), dbg = () =>
|
|
|
560
986
|
recordGrokUsageReport,
|
|
561
987
|
readGrokCycleTotals,
|
|
562
988
|
resetGrokAccounting,
|
|
989
|
+
recordDeepSeekUsageReport,
|
|
990
|
+
readDeepSeekUsageTotals,
|
|
991
|
+
readDeepSeekUsageDays,
|
|
992
|
+
recordDeepSeekBalance,
|
|
993
|
+
readDeepSeekBalanceReferences,
|
|
563
994
|
readClaudeCloudSpend,
|
|
564
995
|
buildReport,
|
|
565
996
|
pools: CURSOR_POOLS,
|
|
@@ -99,6 +99,7 @@ const PROVIDER_LABELS = Object.freeze({
|
|
|
99
99
|
cursor: 'Cursor',
|
|
100
100
|
grok: 'Grok',
|
|
101
101
|
'claude-cloud': 'Claude Cloud',
|
|
102
|
+
deepseek: 'DeepSeek',
|
|
102
103
|
});
|
|
103
104
|
const CLAUDE_PROVIDERS = new Set(['claude', 'claude-cloud']);
|
|
104
105
|
const LONG_CONTEXT_SUFFIX = /\[1m\]$/i;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
import { DEEPSEEK_REASONING_EFFORTS } from '../../shared/deepseek-models.mjs';
|
|
3
4
|
import { openAIReasoningEffortsForModel } from '../../shared/openai-reasoning.mjs';
|
|
4
5
|
|
|
5
6
|
// 'ultracode' is not an Agent SDK EffortLevel: it is a relay-side sentinel for
|
|
@@ -57,6 +58,8 @@ export function supportedReasoningEffortsForProviderModel({
|
|
|
57
58
|
}
|
|
58
59
|
// A cloud session takes no effort choice: the only tier is the default one.
|
|
59
60
|
if (provider === 'claude-cloud') return [...CLAUDE_CLOUD_REASONING_EFFORTS];
|
|
61
|
+
// DeepSeek's levels are the same for every model it serves.
|
|
62
|
+
if (provider === 'deepseek') return [...DEEPSEEK_REASONING_EFFORTS];
|
|
60
63
|
if (provider === 'openai' || provider === 'openai-image') {
|
|
61
64
|
const efforts = openAIReasoningEffortsForModel(model);
|
|
62
65
|
return Array.isArray(efforts) && efforts.length ? efforts.map(normalizeEffort).filter(Boolean) : null;
|
|
@@ -72,18 +75,28 @@ export function supportedReasoningEffortsForProviderModel({
|
|
|
72
75
|
// `strict` reproduces the one provider that refuses rather than clamps: OpenAI
|
|
73
76
|
// answers an unsupported effort with a 400, so bootstrap must not store a value
|
|
74
77
|
// the first send would reject.
|
|
78
|
+
//
|
|
79
|
+
// `defaultEffort` replaces the off-switch as the answer to an unstated effort
|
|
80
|
+
// (DeepSeek thinks at `high` unless told otherwise), and `aliasFor` maps a
|
|
81
|
+
// request into the provider's own levels before it is checked.
|
|
75
82
|
export function resolveProviderReasoningEffort({
|
|
76
83
|
requestedEffort = '',
|
|
77
84
|
supportedEfforts = null,
|
|
78
85
|
strict = false,
|
|
86
|
+
defaultEffort = '',
|
|
87
|
+
aliasFor = null,
|
|
79
88
|
} = {}) {
|
|
80
|
-
const
|
|
89
|
+
const raw = normalizeEffort(requestedEffort);
|
|
90
|
+
const requested = raw && typeof aliasFor === 'function' ? (normalizeEffort(aliasFor(raw)) || raw) : raw;
|
|
81
91
|
const supported = Array.isArray(supportedEfforts)
|
|
82
92
|
? supportedEfforts.map(normalizeEffort).filter(Boolean)
|
|
83
93
|
: null;
|
|
84
94
|
if (!supported) return { ok: true, effort: requested, supported: [] };
|
|
85
95
|
const providerDefault = supported.includes('none') ? 'none' : (supported[0] || '');
|
|
86
|
-
|
|
96
|
+
const preferredDefault = normalizeEffort(defaultEffort);
|
|
97
|
+
if (!requested) {
|
|
98
|
+
return { ok: true, effort: supported.includes(preferredDefault) ? preferredDefault : providerDefault, supported };
|
|
99
|
+
}
|
|
87
100
|
if (supported.includes(requested)) return { ok: true, effort: requested, supported };
|
|
88
101
|
if (strict) {
|
|
89
102
|
return {
|
|
@@ -114,7 +114,7 @@ const TITLE_MAX = 80;
|
|
|
114
114
|
|
|
115
115
|
// The provider settings routes a relay serves; github (Copilot) has none with
|
|
116
116
|
// models — its catalogue is /api/models plus /api/status defaultModel.
|
|
117
|
-
const SETTINGS_PROVIDERS = Object.freeze(['claude', 'cursor', 'grok', 'openai', 'claude-cloud']);
|
|
117
|
+
const SETTINGS_PROVIDERS = Object.freeze(['claude', 'cursor', 'grok', 'openai', 'claude-cloud', 'deepseek']);
|
|
118
118
|
// A Claude Cloud session clones a GitHub repository instead of opening a
|
|
119
119
|
// folder: create_session takes `repo` and `branch` for it.
|
|
120
120
|
const CLOUD_PROVIDER = REMOTE_RELAY_CLOUD_PROVIDER;
|
|
@@ -140,6 +140,7 @@ const CATALOG_PROVIDER_LABELS = Object.freeze({
|
|
|
140
140
|
cursor: 'cursor',
|
|
141
141
|
grok: 'grok',
|
|
142
142
|
'claude-cloud': 'claude-cloud',
|
|
143
|
+
deepseek: 'deepseek',
|
|
143
144
|
});
|
|
144
145
|
// Worker statuses that mean "working on something right now" (scope active).
|
|
145
146
|
const WORKING_WORKER_STATUSES = new Set(['starting', 'processing']);
|