@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.
Files changed (136) hide show
  1. package/.github/extensions/web-relay/polling/polling-loop.mjs +14 -87
  2. package/.github/extensions/web-relay/skills/tool-activity.mjs +5 -4
  3. package/CHANGELOG.md +412 -0
  4. package/README.md +15 -13
  5. package/package.json +2 -1
  6. package/server/claude-api-worker/claude-api-backend.mjs +345 -0
  7. package/server/claude-api-worker/claude-api-session-worker.mjs +29 -0
  8. package/server/claude-worker/claude-attachments.mjs +7 -4
  9. package/server/claude-worker/claude-cli-pinned-env.mjs +150 -0
  10. package/server/claude-worker/claude-remote-relay-tool.mjs +3 -3
  11. package/server/claude-worker/claude-sdk-adapter.mjs +199 -15
  12. package/server/claude-worker/claude-session-process.mjs +276 -33
  13. package/server/claude-worker/claude-session-worker-main.mjs +269 -0
  14. package/server/claude-worker/claude-session-worker.mjs +2 -253
  15. package/server/claude-worker/claude-subscription-key-check.mjs +64 -0
  16. package/server/claude-worker/claude-turn-failure.mjs +8 -0
  17. package/server/claude-worker/claude-turn-publisher.mjs +394 -13
  18. package/server/claude-worker/claude-usage-totals.mjs +161 -0
  19. package/server/claude-worker/sdk-message-normalizer.mjs +56 -1
  20. package/server/copilot-worker/copilot-remote-relay-tool.mjs +2 -2
  21. package/server/copilot-worker/copilot-sdk-adapter.mjs +18 -4
  22. package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +1 -1
  23. package/server/copilot-worker/copilot-sdk-session-process.mjs +80 -14
  24. package/server/cursor-worker/cursor-remote-relay-tool.mjs +3 -3
  25. package/server/cursor-worker/cursor-turn-runner.mjs +4 -4
  26. package/server/cursor-worker/sdk-message-normalizer.mjs +2 -2
  27. package/server/db-schema.mjs +96 -1
  28. package/server/deepseek-worker/deepseek-claude-backend.mjs +317 -0
  29. package/server/deepseek-worker/deepseek-session-worker.mjs +37 -0
  30. package/server/grok-worker/acp-client.mjs +1 -1
  31. package/server/grok-worker/grok-turn-runner.mjs +4 -4
  32. package/server/grok-worker/sdk-message-normalizer.mjs +1 -1
  33. package/server/mcp/oar-mcp-server.mjs +11 -5
  34. package/server/migrations/0024-deepseek-usage-totals.mjs +151 -0
  35. package/server/migrations/0025-deepseek-usage-requests-daily.mjs +179 -0
  36. package/server/migrations/0026-deepseek-usage-served-models.mjs +222 -0
  37. package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +101 -0
  38. package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +99 -0
  39. package/server/migrations/0029-deepseek-balance-rises.mjs +99 -0
  40. package/server/migrations/0030-deepseek-usage-actuals.mjs +123 -0
  41. package/server/migrations/0031-queue-provider.mjs +143 -0
  42. package/server/migrations/0032-runtime-session-billing-switch.mjs +94 -0
  43. package/server/migrations/0033-claude-api-usage-checkpoints.mjs +138 -0
  44. package/server/migrations/0034-claude-api-usage-totals.mjs +112 -0
  45. package/server/public/app/activity-replay-state.mjs +4 -3
  46. package/server/public/app/agent-sessions-settings-ui.js +1 -1
  47. package/server/public/app/agent-sessions-settings.mjs +1 -1
  48. package/server/public/app/api-client.js +105 -0
  49. package/server/public/app/bootstrap.js +243 -29
  50. package/server/public/app/claude-api-settings-ui.js +362 -0
  51. package/server/public/app/claude-billing-toggle-ui.js +224 -0
  52. package/server/public/app/claude-billing-toggle.mjs +112 -0
  53. package/server/public/app/context-tier-options.mjs +10 -2
  54. package/server/public/app/context-usage-view.mjs +1 -1
  55. package/server/public/app/conversation-provider-indicator.mjs +18 -0
  56. package/server/public/app/conversation-view.js +31 -3
  57. package/server/public/app/deepseek-settings-ui.js +342 -0
  58. package/server/public/app/inbox-ui.js +14 -3
  59. package/server/public/app/inbox-view.mjs +1 -1
  60. package/server/public/app/journal-view.js +156 -11
  61. package/server/public/app/model-selector-options.mjs +28 -1
  62. package/server/public/app/model-vision-support.mjs +2 -0
  63. package/server/public/app/new-conversation-model-choice.mjs +21 -0
  64. package/server/public/app/no-conversation-state.mjs +1 -1
  65. package/server/public/app/plan-usage-view.mjs +165 -13
  66. package/server/public/app/provider-readiness.mjs +8 -2
  67. package/server/public/app/relay-error-ctas.mjs +58 -0
  68. package/server/public/app/relay-schedules-ui.js +1 -1
  69. package/server/public/app/settings-modal.js +22 -0
  70. package/server/public/app/settings-tabs.js +1 -1
  71. package/server/public/app/socket-handlers.js +28 -1
  72. package/server/public/app/tool-activity-emoji.mjs +4 -3
  73. package/server/public/app/usage-limit-ui.js +136 -5
  74. package/server/public/index.html +596 -35
  75. package/server/relay-tools.md +4 -4
  76. package/server/repositories/conversation-group-repository.mjs +14 -7
  77. package/server/repositories/message-repository.mjs +7 -0
  78. package/server/repositories/session-repository.mjs +8 -1
  79. package/server/routes/ask-user-routes.mjs +1 -1
  80. package/server/routes/conversation-group-routes.mjs +3 -2
  81. package/server/routes/deepseek-usage-import-routes.mjs +72 -0
  82. package/server/routes/messages-routes.mjs +831 -36
  83. package/server/routes/relay-ask-routes.mjs +3 -3
  84. package/server/routes/relay-event-routes.mjs +2 -2
  85. package/server/routes/relay-schedule-routes.mjs +1 -1
  86. package/server/routes/remote-relay-routes.mjs +3 -3
  87. package/server/routes/sessions-routes.mjs +670 -63
  88. package/server/server-runtime.mjs +476 -26
  89. package/server/services/about-service.mjs +2 -0
  90. package/server/services/background-work-view.mjs +2 -1
  91. package/server/services/claude-api-settings-service.mjs +575 -0
  92. package/server/services/claude-api-usage-service.mjs +361 -0
  93. package/server/services/claude-billing-swap-service.mjs +274 -0
  94. package/server/services/claude-billing-switch-service.mjs +343 -0
  95. package/server/services/claude-session-leftovers.mjs +226 -0
  96. package/server/services/deepseek-balance-service.mjs +102 -0
  97. package/server/services/deepseek-settings-service.mjs +380 -0
  98. package/server/services/deepseek-usage-import.mjs +471 -0
  99. package/server/services/model-variant-catalog-service.mjs +1 -0
  100. package/server/services/plan-usage-claude-api.mjs +445 -0
  101. package/server/services/plan-usage-contract.mjs +100 -5
  102. package/server/services/plan-usage-deepseek.mjs +931 -0
  103. package/server/services/plan-usage-service.mjs +436 -0
  104. package/server/services/provider-model-selection.mjs +4 -2
  105. package/server/services/provider-reasoning-effort.mjs +24 -2
  106. package/server/services/relay-ask-service.mjs +1 -1
  107. package/server/services/relay-event-service.mjs +3 -2
  108. package/server/services/relay-schedule-service.mjs +1 -1
  109. package/server/services/remote-relay-caller-context.mjs +7 -0
  110. package/server/services/remote-relay-dispatcher.mjs +84 -14
  111. package/server/services/remote-relay-registry-service.mjs +1 -1
  112. package/server/services/session-worker-launch-service.mjs +294 -84
  113. package/server/services/session-worker-registry-service.mjs +28 -1
  114. package/server/services/session-worker-websocket-service.mjs +17 -0
  115. package/server/services/usage-limit-pause-service.mjs +55 -4
  116. package/server/services/worker-secret-redaction.mjs +320 -0
  117. package/shared/anthropic-api.mjs +202 -0
  118. package/shared/context-window-fallbacks.mjs +3 -0
  119. package/shared/deepseek-api.mjs +137 -0
  120. package/shared/deepseek-models.mjs +114 -0
  121. package/shared/deepseek-pricing.mjs +146 -0
  122. package/shared/host-claude-session-env.mjs +101 -0
  123. package/shared/model-id.mjs +7 -0
  124. package/shared/preview-tool-core.mjs +1 -1
  125. package/shared/provider-routing.mjs +5 -3
  126. package/shared/relay-asks.mjs +11 -5
  127. package/shared/relay-events.mjs +2 -2
  128. package/shared/relay-schedules.mjs +2 -2
  129. package/shared/remote-relay-contract.mjs +29 -9
  130. package/shared/remote-relay-tool-core.mjs +23 -6
  131. package/shared/steer-settle-failure.mjs +2 -0
  132. package/shared/worker-runtime/api-client.mjs +2 -2
  133. package/shared/worker-runtime/direct-openai-image.mjs +128 -0
  134. package/shared/worker-runtime/heartbeat.mjs +5 -0
  135. package/shared/worker-runtime/worker-log-file.mjs +60 -0
  136. package/shared/worker-runtime/worker-websocket-link.mjs +6 -0
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Migration: Claude API usage totals (services/plan-usage-service.mjs, the
5
+ * Claude API card in Check Usage and its monthly cap).
6
+ *
7
+ * `claude_api_usage_totals`: what this relay's turns on the saved Claude API
8
+ * key ran, per day and model; a row only ever grows. Subscription turns are
9
+ * never booked here. The card shows today and this month, and the cap is a
10
+ * calendar month in relay local time, so the day is the relay's local date.
11
+ *
12
+ * - `day`: `YYYY-MM-DD`, the relay's local date of the turn's result.
13
+ * - `model`: the model's id as the CLI reported it, without the `[1m]`
14
+ * suffix, lower case.
15
+ * - `hit_tokens`, `miss_tokens`, `output_tokens`: input read from the
16
+ * prompt cache, other input (cache writes included), and output.
17
+ * - `cost_usd`: Claude Code's own estimate (`modelUsage[*].costUSD`) of what
18
+ * the booked turns cost, for the turns it could price.
19
+ * - `unknown_cost_tokens`: the tokens of turns it could not price
20
+ * (`costBasis: unknown`), which `cost_usd` leaves out.
21
+ * - `turn_count`: the posts that booked something for the model.
22
+ * - `first_seen_at`, `last_seen_at`, `updated_at`.
23
+ *
24
+ * Additive and idempotent. Safe to run on every boot.
25
+ *
26
+ * Usage: node server/migrations/0034-claude-api-usage-totals.mjs [path/to/copilot.db]
27
+ */
28
+
29
+ import { fileURLToPath } from 'url';
30
+ import path from 'path';
31
+ import process from 'process';
32
+ import Database from 'better-sqlite3';
33
+
34
+ export const CLAUDE_API_USAGE_TOTALS_TABLE = 'claude_api_usage_totals';
35
+
36
+ export const CLAUDE_API_USAGE_TOTALS_COLUMNS = Object.freeze([
37
+ 'day',
38
+ 'model',
39
+ 'hit_tokens',
40
+ 'miss_tokens',
41
+ 'output_tokens',
42
+ 'cost_usd',
43
+ 'unknown_cost_tokens',
44
+ 'turn_count',
45
+ 'first_seen_at',
46
+ 'last_seen_at',
47
+ 'updated_at',
48
+ ]);
49
+
50
+ function tableExists(db, name) {
51
+ return !!db
52
+ .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
53
+ .get(name);
54
+ }
55
+
56
+ /** What keeps the service from using the table: '' when nothing does. */
57
+ export function claudeApiUsageTotalsProblem(db) {
58
+ if (!tableExists(db, CLAUDE_API_USAGE_TOTALS_TABLE)) return `no ${CLAUDE_API_USAGE_TOTALS_TABLE} table`;
59
+ const present = new Set(db.prepare(`PRAGMA table_info(${CLAUDE_API_USAGE_TOTALS_TABLE})`).all().map((column) => column.name));
60
+ const missing = CLAUDE_API_USAGE_TOTALS_COLUMNS.filter((name) => !present.has(name));
61
+ if (missing.length) return `${CLAUDE_API_USAGE_TOTALS_TABLE} lacks ${missing.join(', ')}`;
62
+ return '';
63
+ }
64
+
65
+ export function migrateClaudeApiUsageTotals(db) {
66
+ if (tableExists(db, CLAUDE_API_USAGE_TOTALS_TABLE)) return { applied: false, tablesAdded: 0 };
67
+ db.exec(`
68
+ CREATE TABLE ${CLAUDE_API_USAGE_TOTALS_TABLE} (
69
+ day TEXT NOT NULL,
70
+ model TEXT NOT NULL,
71
+ hit_tokens INTEGER NOT NULL DEFAULT 0,
72
+ miss_tokens INTEGER NOT NULL DEFAULT 0,
73
+ output_tokens INTEGER NOT NULL DEFAULT 0,
74
+ cost_usd REAL NOT NULL DEFAULT 0,
75
+ unknown_cost_tokens INTEGER NOT NULL DEFAULT 0,
76
+ turn_count INTEGER NOT NULL DEFAULT 0,
77
+ first_seen_at TEXT NOT NULL,
78
+ last_seen_at TEXT NOT NULL,
79
+ updated_at TEXT NOT NULL,
80
+ PRIMARY KEY (day, model)
81
+ )
82
+ `);
83
+ return { applied: true, tablesAdded: 1 };
84
+ }
85
+
86
+ function defaultDbPath() {
87
+ const here = path.dirname(fileURLToPath(import.meta.url));
88
+ return path.resolve(here, '..', 'data', 'copilot.db');
89
+ }
90
+
91
+ export function migrate(dbPath, options = {}) {
92
+ const db = new Database(dbPath, options);
93
+ try {
94
+ return migrateClaudeApiUsageTotals(db);
95
+ } finally {
96
+ db.close();
97
+ }
98
+ }
99
+
100
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
101
+ if (isMain) {
102
+ const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
103
+ try {
104
+ const result = migrate(dbPath);
105
+ console.log(`Migration 0034 on ${dbPath}`);
106
+ console.log(result.applied ? ` claude api usage totals: tables added ${result.tablesAdded}` : ' skipped: nothing to change');
107
+ process.exit(0);
108
+ } catch (err) {
109
+ console.error(`Migration 0034 failed: ${err?.message || err}`);
110
+ process.exit(1);
111
+ }
112
+ }
@@ -26,12 +26,13 @@ export function isCompactBoundaryActivityEntry(item) {
26
26
 
27
27
  // The live list of one turn keeps its newest ordinary lines only, but every
28
28
  // structured row a line of the transcript is built from: the turn's
29
- // compaction entries and the session-restart note would otherwise scroll out
30
- // of a busy turn and take their line with them until the next reload.
29
+ // compaction entries and the session-restart notes (a compaction window, a
30
+ // billing switch) would otherwise scroll out of a busy turn and take their
31
+ // line with them until the next reload.
31
32
  export const LIVE_ACTIVITY_CAP = 24;
32
33
  function isKeptLiveActivityEntry(item) {
33
34
  const kind = normalizeRelayActivityEntry(item)?.metadata?.kind;
34
- return kind === 'compact_boundary' || kind === 'compact_window_respawn';
35
+ return kind === 'compact_boundary' || kind === 'compact_window_respawn' || kind === 'billing_switch_respawn';
35
36
  }
36
37
  export function capLiveActivityEntries(items, cap = LIVE_ACTIVITY_CAP) {
37
38
  const list = Array.isArray(items) ? items : [];
@@ -1,5 +1,5 @@
1
1
  // Settings → Relays → Agent sessions: the switch that lets agents start and use
2
- // sessions on this relay through the remote_relay tool, and the slider for the
2
+ // sessions on this relay through the relay_tool, and the slider for the
3
3
  // longest wait of one tool call. The decisions live in
4
4
  // agent-sessions-settings.mjs; this file only moves them into the markup in
5
5
  // index.html. The static controls use inline handlers that bootstrap.js
@@ -1,5 +1,5 @@
1
1
  // Pure helpers for Settings → Relays → Agent sessions: whether agents may
2
- // start and use sessions on this relay, and the longest time one remote_relay
2
+ // start and use sessions on this relay, and the longest time one relay_tool
3
3
  // call may wait. DOM-free on purpose, like claude-cloud-ui.mjs: the section
4
4
  // (agent-sessions-settings-ui.js) owns the elements, this module the decisions.
5
5
  //
@@ -530,6 +530,90 @@ export async function updateCursorSettings({
530
530
  return settingsRequest('/api/settings/cursor', payload, 'Failed to update Cursor settings');
531
531
  }
532
532
 
533
+ export async function loadDeepSeekSettings() {
534
+ return apiFetch('/api/settings/deepseek');
535
+ }
536
+
537
+ /**
538
+ * Saves what the caller names: a key (written, never read back), the switch,
539
+ * the default model, the enabled models, the base URL (null = DeepSeek's
540
+ * own), the attribution and the monthly budget (null = none); `remove`
541
+ * takes the key out.
542
+ */
543
+ export async function updateDeepSeekSettings({
544
+ apiKey = '',
545
+ model = undefined,
546
+ enabled = undefined,
547
+ enabledModels = undefined,
548
+ baseUrl = undefined,
549
+ attribution = undefined,
550
+ monthlyBudget = undefined,
551
+ monthlyBudgetCurrency = undefined,
552
+ startingBalance = undefined,
553
+ startingBalanceEdited = undefined,
554
+ remove = false,
555
+ } = {}) {
556
+ // Only the fields named here reach the relay: a field the DeepSeek tab
557
+ // shows needs its line below (api-client.deepseek-settings.test.mjs).
558
+ const payload = remove === true ? { remove: true } : {};
559
+ if (!payload.remove) {
560
+ const key = String(apiKey || '').trim();
561
+ if (key) payload.apiKey = key;
562
+ if (typeof model === 'string' && model.trim()) payload.model = model.trim();
563
+ if (typeof enabled === 'boolean') payload.enabled = enabled;
564
+ if (Array.isArray(enabledModels)) payload.enabledModels = enabledModels;
565
+ if (baseUrl === null || typeof baseUrl === 'string') payload.baseUrl = baseUrl === null ? null : String(baseUrl).trim();
566
+ if (typeof attribution === 'string' && attribution.trim()) payload.attribution = attribution.trim();
567
+ if (monthlyBudget === null || (typeof monthlyBudget === 'number' && Number.isFinite(monthlyBudget))) payload.monthlyBudget = monthlyBudget;
568
+ // 'USD' or 'CNY'; null is the balance's currency.
569
+ if (monthlyBudgetCurrency === null || typeof monthlyBudgetCurrency === 'string') {
570
+ payload.monthlyBudgetCurrency = String(monthlyBudgetCurrency ?? '').trim() || null;
571
+ }
572
+ if (startingBalance === null || (typeof startingBalance === 'number' && Number.isFinite(startingBalance))) payload.startingBalance = startingBalance;
573
+ if (typeof startingBalanceEdited === 'boolean') payload.startingBalanceEdited = startingBalanceEdited;
574
+ }
575
+ return settingsRequest('/api/settings/deepseek', payload, 'Failed to update DeepSeek settings');
576
+ }
577
+
578
+ export async function loadClaudeApiSettings() {
579
+ return apiFetch('/api/settings/claude-api');
580
+ }
581
+
582
+ /**
583
+ * Saves what the caller names on the Claude API tab: a key (written, never
584
+ * read back), the switch, the default model, the enabled models and the
585
+ * monthly cap in US dollars (null = no cap); `remove` takes the key out.
586
+ */
587
+ export async function updateClaudeApiSettings({
588
+ apiKey = '',
589
+ model = undefined,
590
+ enabled = undefined,
591
+ enabledModels = undefined,
592
+ monthlyCapUsd = undefined,
593
+ remove = false,
594
+ } = {}) {
595
+ // Only the fields named here reach the relay: a field the Claude API tab
596
+ // shows needs its line below (api-client.claude-api-settings.test.mjs).
597
+ const payload = remove === true ? { remove: true } : {};
598
+ if (!payload.remove) {
599
+ const key = String(apiKey || '').trim();
600
+ if (key) payload.apiKey = key;
601
+ if (typeof model === 'string' && model.trim()) payload.model = model.trim();
602
+ if (typeof enabled === 'boolean') payload.enabled = enabled;
603
+ if (Array.isArray(enabledModels)) payload.enabledModels = enabledModels;
604
+ if (monthlyCapUsd === null || (typeof monthlyCapUsd === 'number' && Number.isFinite(monthlyCapUsd))) payload.monthlyCapUsd = monthlyCapUsd;
605
+ }
606
+ return settingsRequest('/api/settings/claude-api', payload, 'Failed to update Claude API settings');
607
+ }
608
+
609
+ /**
610
+ * Imports DeepSeek's usage export: the texts of its amount and cost files.
611
+ * Answers `{ ok, summary }`; a refused export throws with the relay's reason.
612
+ */
613
+ export async function importDeepSeekUsageExport({ amount, cost }) {
614
+ return settingsRequest('/api/deepseek-usage-import', { amount, cost }, 'Failed to import the DeepSeek usage export');
615
+ }
616
+
533
617
  export async function loadWindowsAutostartSetting() {
534
618
  return apiFetchOrThrow('/api/settings/windows-autostart');
535
619
  }
@@ -1426,6 +1510,27 @@ export async function resumeUsageLimitPause(conversationId) {
1426
1510
  });
1427
1511
  }
1428
1512
 
1513
+ // Move a Claude chat between the subscription ("claude") and the saved API
1514
+ // key ("claude-api"), from its next turn. `by: "usage-limit"` is the
1515
+ // usage-limit note's button. `{ ok: true, ... }`, else `{ ok: false, error,
1516
+ // code }` with the relay's reason (the key, the model, the cap).
1517
+ export async function switchConversationBilling(conversationId, { to, by = 'user' } = {}) {
1518
+ let refusal = null;
1519
+ const result = await apiFetch(`/api/conversation/${encodeURIComponent(String(conversationId || ''))}/billing`, {
1520
+ method: 'POST',
1521
+ body: JSON.stringify({ to, by }),
1522
+ onErrorResponse: async (response) => {
1523
+ const payload = await response.json().catch(() => null);
1524
+ refusal = {
1525
+ ok: false,
1526
+ error: readableServerError(payload?.error) || `The relay answered ${response.status}.`,
1527
+ code: payload?.code || null,
1528
+ };
1529
+ },
1530
+ });
1531
+ return result || refusal || { ok: false, error: 'The relay could not be reached.', code: null };
1532
+ }
1533
+
1429
1534
  export async function cancelUsageLimitPause(conversationId) {
1430
1535
  return apiFetch('/api/usage-limit/cancel', {
1431
1536
  method: 'POST',