@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
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration: the DeepSeek balance reference notes when the balance last rose
|
|
5
|
+
* (services/plan-usage-service.mjs, the DeepSeek card's balance bar). Builds
|
|
6
|
+
* on 0025.
|
|
7
|
+
*
|
|
8
|
+
* `deepseek_balance_reference` gains these columns:
|
|
9
|
+
* - `rose_at`: when a read last found the balance above the read before it
|
|
10
|
+
* (a top-up or a grant); NULL while no rise was seen. `reference_at` cannot
|
|
11
|
+
* tell: it is also set by the first read of a currency. A starting balance
|
|
12
|
+
* set on the DeepSeek tab counts until the balance rises after it was set
|
|
13
|
+
* (plan-usage-deepseek.mjs).
|
|
14
|
+
* - `last_read_at`: when `last_total` was read; NULL for a row from before.
|
|
15
|
+
* - `rose_from_at`: when the balance before that rise was read; NULL when
|
|
16
|
+
* unknown. A rise the relay first sees after the starting balance was set
|
|
17
|
+
* may be a top-up from before it, unless the balance before the rise was
|
|
18
|
+
* read after it too.
|
|
19
|
+
*
|
|
20
|
+
* Additive and idempotent. Safe to run on every boot.
|
|
21
|
+
*
|
|
22
|
+
* Usage: node server/migrations/0029-deepseek-balance-rises.mjs [path/to/copilot.db]
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { fileURLToPath } from 'url';
|
|
26
|
+
import path from 'path';
|
|
27
|
+
import process from 'process';
|
|
28
|
+
import Database from 'better-sqlite3';
|
|
29
|
+
|
|
30
|
+
import { DEEPSEEK_BALANCE_REFERENCE_TABLE, migrateDeepSeekUsageRequestsDaily } from './0025-deepseek-usage-requests-daily.mjs';
|
|
31
|
+
|
|
32
|
+
// Added to deepseek_balance_reference, with their DDL.
|
|
33
|
+
export const DEEPSEEK_BALANCE_RISE_COLUMNS = Object.freeze([
|
|
34
|
+
['rose_at', 'TEXT'],
|
|
35
|
+
['last_read_at', 'TEXT'],
|
|
36
|
+
['rose_from_at', 'TEXT'],
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
function tableExists(db, name) {
|
|
40
|
+
return !!db
|
|
41
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
42
|
+
.get(name);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function columnNames(db, table) {
|
|
46
|
+
return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((column) => column.name));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** What keeps the service from using the column: '' when nothing does. */
|
|
50
|
+
export function deepseekBalanceRisesProblem(db) {
|
|
51
|
+
if (!tableExists(db, DEEPSEEK_BALANCE_REFERENCE_TABLE)) return `no ${DEEPSEEK_BALANCE_REFERENCE_TABLE} table`;
|
|
52
|
+
const present = columnNames(db, DEEPSEEK_BALANCE_REFERENCE_TABLE);
|
|
53
|
+
const missing = DEEPSEEK_BALANCE_RISE_COLUMNS.map(([name]) => name).filter((name) => !present.has(name));
|
|
54
|
+
if (missing.length) return `${DEEPSEEK_BALANCE_REFERENCE_TABLE} lacks ${missing.join(', ')}`;
|
|
55
|
+
return '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function migrateDeepSeekBalanceRises(db) {
|
|
59
|
+
// The column goes on 0025's table: a database that skipped 0025 gets it first.
|
|
60
|
+
migrateDeepSeekUsageRequestsDaily(db);
|
|
61
|
+
let columnsAdded = 0;
|
|
62
|
+
const present = columnNames(db, DEEPSEEK_BALANCE_REFERENCE_TABLE);
|
|
63
|
+
for (const [name, ddl] of DEEPSEEK_BALANCE_RISE_COLUMNS) {
|
|
64
|
+
if (present.has(name)) continue;
|
|
65
|
+
db.exec(`ALTER TABLE ${DEEPSEEK_BALANCE_REFERENCE_TABLE} ADD COLUMN ${name} ${ddl}`);
|
|
66
|
+
columnsAdded += 1;
|
|
67
|
+
}
|
|
68
|
+
return { applied: columnsAdded > 0, columnsAdded };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function defaultDbPath() {
|
|
72
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
73
|
+
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function migrate(dbPath, options = {}) {
|
|
77
|
+
const db = new Database(dbPath, options);
|
|
78
|
+
try {
|
|
79
|
+
return migrateDeepSeekBalanceRises(db);
|
|
80
|
+
} finally {
|
|
81
|
+
db.close();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
86
|
+
if (isMain) {
|
|
87
|
+
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
88
|
+
try {
|
|
89
|
+
const result = migrate(dbPath);
|
|
90
|
+
console.log(`Migration 0029 on ${dbPath}`);
|
|
91
|
+
console.log(result.applied
|
|
92
|
+
? ` deepseek balance rises: columns added ${result.columnsAdded}`
|
|
93
|
+
: ' skipped: nothing to change');
|
|
94
|
+
process.exit(0);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error(`Migration 0029 failed: ${err?.message || err}`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration: the figures of DeepSeek's own usage export, imported from the
|
|
5
|
+
* platform's two CSV files (services/deepseek-usage-import.mjs). The usage
|
|
6
|
+
* card in Check Usage shows them instead of the relay's estimate for the
|
|
7
|
+
* days and models they cover.
|
|
8
|
+
*
|
|
9
|
+
* Adds one table of its own, `deepseek_usage_actuals`, one row per export
|
|
10
|
+
* day, model and API key (the key is the day, so a re-export taken in
|
|
11
|
+
* another time zone replaces the day instead of adding a second row):
|
|
12
|
+
* - `day`: the local date of `period_start`, in the export's time zone;
|
|
13
|
+
* - `period_start`, `period_end`: the first start and last end of the day's
|
|
14
|
+
* periods as the export gives them (ISO with the export's offset);
|
|
15
|
+
* - `model`, `key_fp` (the key masked as `sk-XXXXX…YYYY`), `key_name`;
|
|
16
|
+
* - `hit_tokens`, `miss_tokens`, `output_tokens`, `request_count`;
|
|
17
|
+
* - `cost`, `currency`: the period's cost, amount × price summed;
|
|
18
|
+
* - `imported_at`.
|
|
19
|
+
*
|
|
20
|
+
* The relay's own usage tables are not touched. Additive and idempotent.
|
|
21
|
+
* Safe to run on every boot.
|
|
22
|
+
*
|
|
23
|
+
* Usage: node server/migrations/0030-deepseek-usage-actuals.mjs [path/to/copilot.db]
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { fileURLToPath } from 'url';
|
|
27
|
+
import path from 'path';
|
|
28
|
+
import process from 'process';
|
|
29
|
+
import Database from 'better-sqlite3';
|
|
30
|
+
|
|
31
|
+
export const DEEPSEEK_USAGE_ACTUALS_TABLE = 'deepseek_usage_actuals';
|
|
32
|
+
|
|
33
|
+
export const DEEPSEEK_USAGE_ACTUALS_COLUMNS = Object.freeze([
|
|
34
|
+
'period_start',
|
|
35
|
+
'period_end',
|
|
36
|
+
'day',
|
|
37
|
+
'model',
|
|
38
|
+
'key_fp',
|
|
39
|
+
'key_name',
|
|
40
|
+
'hit_tokens',
|
|
41
|
+
'miss_tokens',
|
|
42
|
+
'output_tokens',
|
|
43
|
+
'request_count',
|
|
44
|
+
'cost',
|
|
45
|
+
'currency',
|
|
46
|
+
'imported_at',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const CREATE_SQL = `
|
|
50
|
+
CREATE TABLE IF NOT EXISTS ${DEEPSEEK_USAGE_ACTUALS_TABLE} (
|
|
51
|
+
period_start TEXT NOT NULL,
|
|
52
|
+
period_end TEXT NOT NULL,
|
|
53
|
+
day TEXT NOT NULL,
|
|
54
|
+
model TEXT NOT NULL,
|
|
55
|
+
key_fp TEXT NOT NULL,
|
|
56
|
+
key_name TEXT NOT NULL DEFAULT '',
|
|
57
|
+
hit_tokens INTEGER NOT NULL DEFAULT 0,
|
|
58
|
+
miss_tokens INTEGER NOT NULL DEFAULT 0,
|
|
59
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
60
|
+
request_count INTEGER NOT NULL DEFAULT 0,
|
|
61
|
+
cost REAL NOT NULL DEFAULT 0,
|
|
62
|
+
currency TEXT NOT NULL DEFAULT '',
|
|
63
|
+
imported_at TEXT NOT NULL,
|
|
64
|
+
PRIMARY KEY (day, model, key_fp)
|
|
65
|
+
);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_${DEEPSEEK_USAGE_ACTUALS_TABLE}_key_day
|
|
67
|
+
ON ${DEEPSEEK_USAGE_ACTUALS_TABLE} (key_fp, day);
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
function tableExists(db, name) {
|
|
71
|
+
return !!db
|
|
72
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
73
|
+
.get(name);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function columnNames(db, table) {
|
|
77
|
+
return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((column) => column.name));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** What keeps the import from using the table: '' when nothing does. */
|
|
81
|
+
export function deepseekUsageActualsProblem(db) {
|
|
82
|
+
if (!tableExists(db, DEEPSEEK_USAGE_ACTUALS_TABLE)) return `no ${DEEPSEEK_USAGE_ACTUALS_TABLE} table`;
|
|
83
|
+
const present = columnNames(db, DEEPSEEK_USAGE_ACTUALS_TABLE);
|
|
84
|
+
const missing = DEEPSEEK_USAGE_ACTUALS_COLUMNS.filter((name) => !present.has(name));
|
|
85
|
+
if (missing.length) return `${DEEPSEEK_USAGE_ACTUALS_TABLE} lacks ${missing.join(', ')}`;
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function migrateDeepSeekUsageActuals(db) {
|
|
90
|
+
const existed = tableExists(db, DEEPSEEK_USAGE_ACTUALS_TABLE);
|
|
91
|
+
db.exec(CREATE_SQL);
|
|
92
|
+
return { applied: !existed, tableCreated: !existed };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function defaultDbPath() {
|
|
96
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
97
|
+
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function migrate(dbPath, options = {}) {
|
|
101
|
+
const db = new Database(dbPath, options);
|
|
102
|
+
try {
|
|
103
|
+
return migrateDeepSeekUsageActuals(db);
|
|
104
|
+
} finally {
|
|
105
|
+
db.close();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
110
|
+
if (isMain) {
|
|
111
|
+
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
112
|
+
try {
|
|
113
|
+
const result = migrate(dbPath);
|
|
114
|
+
console.log(`Migration 0030 on ${dbPath}`);
|
|
115
|
+
console.log(result.applied
|
|
116
|
+
? ' deepseek usage actuals: table created'
|
|
117
|
+
: ' skipped: nothing to change');
|
|
118
|
+
process.exit(0);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.error(`Migration 0030 failed: ${err?.message || err}`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration: a queue row names the billing it runs on (the Claude billing
|
|
5
|
+
* switch, services/claude-billing-switch-service.mjs).
|
|
6
|
+
*
|
|
7
|
+
* `queue.provider`: the provider the row was queued under, written at every
|
|
8
|
+
* insert from the chat's `runtime_sessions.provider_type`. A Claude chat can
|
|
9
|
+
* move between `claude` (the subscription) and `claude-api` (the saved API
|
|
10
|
+
* key); the switch re-stamps the chat's rows that have not started, and every
|
|
11
|
+
* requeue re-stamps to the chat's billing of that moment. The relay hands a
|
|
12
|
+
* row only to a worker of that kind, so a turn never runs on a billing nobody
|
|
13
|
+
* asked for. NULL (a row from before this column) means the chat's billing.
|
|
14
|
+
*
|
|
15
|
+
* Two triggers keep the stamp without every writer having to know it:
|
|
16
|
+
* - `queue_provider_after_insert`: a row inserted without a provider (the
|
|
17
|
+
* send route, schedules, events, restart-resume, the board, the usage-limit
|
|
18
|
+
* follow-up) takes its chat's provider when the chat is a Claude or Claude
|
|
19
|
+
* API one. A writer that names one (a continuation: the kind of the worker
|
|
20
|
+
* that runs it) keeps it.
|
|
21
|
+
* - `queue_provider_after_requeue`: a row going back from processing to
|
|
22
|
+
* pending or parked (the stale sweep, dead-worker recovery, a hand-back) is
|
|
23
|
+
* a new turn, and takes the chat's provider of that moment.
|
|
24
|
+
*
|
|
25
|
+
* The back-fill gives the unfinished rows of Claude and Claude API chats
|
|
26
|
+
* (pending, parked, processing) their chat's provider. It touches only rows
|
|
27
|
+
* without one, so it is idempotent; finished rows stay NULL.
|
|
28
|
+
*
|
|
29
|
+
* Additive and idempotent. Safe to run on every boot.
|
|
30
|
+
*
|
|
31
|
+
* Usage: node server/migrations/0031-queue-provider.mjs [path/to/copilot.db]
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { fileURLToPath } from 'url';
|
|
35
|
+
import path from 'path';
|
|
36
|
+
import process from 'process';
|
|
37
|
+
import Database from 'better-sqlite3';
|
|
38
|
+
|
|
39
|
+
export const QUEUE_PROVIDER_COLUMN = 'provider';
|
|
40
|
+
export const QUEUE_PROVIDER_TRIGGERS = Object.freeze(['queue_provider_after_insert', 'queue_provider_after_requeue']);
|
|
41
|
+
|
|
42
|
+
// The chat's billing, when it is a Claude one: NULL for every other provider.
|
|
43
|
+
const chatClaudeBillingSql = (conversationIdSql) => `(
|
|
44
|
+
SELECT LOWER(TRIM(rs.provider_type)) FROM runtime_sessions rs
|
|
45
|
+
WHERE rs.conversation_id = ${conversationIdSql}
|
|
46
|
+
AND LOWER(TRIM(rs.provider_type)) IN ('claude', 'claude-api')
|
|
47
|
+
)`;
|
|
48
|
+
|
|
49
|
+
function tableExists(db, name) {
|
|
50
|
+
return !!db
|
|
51
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
52
|
+
.get(name);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function columnNames(db, table) {
|
|
56
|
+
return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((column) => column.name));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** What keeps the relay from stamping rows: '' when nothing does. */
|
|
60
|
+
export function queueProviderProblem(db) {
|
|
61
|
+
if (!tableExists(db, 'queue')) return 'no queue table';
|
|
62
|
+
if (!columnNames(db, 'queue').has(QUEUE_PROVIDER_COLUMN)) return 'queue lacks provider';
|
|
63
|
+
const triggers = new Set(db.prepare(`SELECT name FROM sqlite_master WHERE type = 'trigger' AND tbl_name = 'queue'`).all().map((row) => row.name));
|
|
64
|
+
const missing = QUEUE_PROVIDER_TRIGGERS.filter((name) => !triggers.has(name));
|
|
65
|
+
if (missing.length) return `queue lacks the trigger ${missing.join(', ')}`;
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function migrateQueueProvider(db) {
|
|
70
|
+
if (!tableExists(db, 'queue')) return { applied: false, columnsAdded: 0, triggersAdded: 0, rowsFilled: 0 };
|
|
71
|
+
// The triggers and the back-fill read provider_type: a table without it
|
|
72
|
+
// waits for the schema boot to add it.
|
|
73
|
+
if (!tableExists(db, 'runtime_sessions') || !columnNames(db, 'runtime_sessions').has('provider_type')) {
|
|
74
|
+
return { applied: false, columnsAdded: 0, triggersAdded: 0, rowsFilled: 0 };
|
|
75
|
+
}
|
|
76
|
+
let columnsAdded = 0;
|
|
77
|
+
if (!columnNames(db, 'queue').has(QUEUE_PROVIDER_COLUMN)) {
|
|
78
|
+
db.exec(`ALTER TABLE queue ADD COLUMN ${QUEUE_PROVIDER_COLUMN} TEXT`);
|
|
79
|
+
columnsAdded += 1;
|
|
80
|
+
}
|
|
81
|
+
const present = new Set(db.prepare(`SELECT name FROM sqlite_master WHERE type = 'trigger' AND tbl_name = 'queue'`).all().map((row) => row.name));
|
|
82
|
+
let triggersAdded = 0;
|
|
83
|
+
if (!present.has('queue_provider_after_insert')) {
|
|
84
|
+
db.exec(`
|
|
85
|
+
CREATE TRIGGER queue_provider_after_insert
|
|
86
|
+
AFTER INSERT ON queue
|
|
87
|
+
WHEN NEW.provider IS NULL
|
|
88
|
+
BEGIN
|
|
89
|
+
UPDATE queue SET provider = ${chatClaudeBillingSql('NEW.conversation_id')} WHERE id = NEW.id;
|
|
90
|
+
END
|
|
91
|
+
`);
|
|
92
|
+
triggersAdded += 1;
|
|
93
|
+
}
|
|
94
|
+
if (!present.has('queue_provider_after_requeue')) {
|
|
95
|
+
db.exec(`
|
|
96
|
+
CREATE TRIGGER queue_provider_after_requeue
|
|
97
|
+
AFTER UPDATE OF status ON queue
|
|
98
|
+
WHEN OLD.status = 'processing' AND NEW.status IN ('pending', 'parked')
|
|
99
|
+
BEGIN
|
|
100
|
+
UPDATE queue SET provider = COALESCE(${chatClaudeBillingSql('NEW.conversation_id')}, NEW.provider) WHERE id = NEW.id;
|
|
101
|
+
END
|
|
102
|
+
`);
|
|
103
|
+
triggersAdded += 1;
|
|
104
|
+
}
|
|
105
|
+
const rowsFilled = db.prepare(`
|
|
106
|
+
UPDATE queue
|
|
107
|
+
SET provider = ${chatClaudeBillingSql('queue.conversation_id')}
|
|
108
|
+
WHERE provider IS NULL
|
|
109
|
+
AND status IN ('pending', 'parked', 'processing')
|
|
110
|
+
AND ${chatClaudeBillingSql('queue.conversation_id')} IS NOT NULL
|
|
111
|
+
`).run().changes;
|
|
112
|
+
return { applied: columnsAdded > 0 || triggersAdded > 0 || rowsFilled > 0, columnsAdded, triggersAdded, rowsFilled };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function defaultDbPath() {
|
|
116
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
117
|
+
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function migrate(dbPath, options = {}) {
|
|
121
|
+
const db = new Database(dbPath, options);
|
|
122
|
+
try {
|
|
123
|
+
return migrateQueueProvider(db);
|
|
124
|
+
} finally {
|
|
125
|
+
db.close();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
130
|
+
if (isMain) {
|
|
131
|
+
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
132
|
+
try {
|
|
133
|
+
const result = migrate(dbPath);
|
|
134
|
+
console.log(`Migration 0031 on ${dbPath}`);
|
|
135
|
+
console.log(result.applied
|
|
136
|
+
? ` queue provider: columns added ${result.columnsAdded}, triggers added ${result.triggersAdded}, rows filled ${result.rowsFilled}`
|
|
137
|
+
: ' skipped: nothing to change');
|
|
138
|
+
process.exit(0);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.error(`Migration 0031 failed: ${err?.message || err}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration: a runtime session remembers its last billing switch (the Claude
|
|
5
|
+
* billing switch, services/claude-billing-switch-service.mjs).
|
|
6
|
+
*
|
|
7
|
+
* `runtime_sessions` gains two columns:
|
|
8
|
+
* - `billing_switched_at`: when the chat last moved between `claude` (the
|
|
9
|
+
* subscription) and `claude-api` (the saved API key); NULL for a chat that
|
|
10
|
+
* never did. A worker started before the switch existed reports no kind, and
|
|
11
|
+
* on a chat with this set the relay cannot assume the kind from
|
|
12
|
+
* `provider_type`: it replaces that worker once, when it is idle. A chat
|
|
13
|
+
* never switched has had one billing, which also settles whose turns its
|
|
14
|
+
* older messages are.
|
|
15
|
+
* - `billing_switched_by`: who asked: `user` (the toggle), `agent` (an
|
|
16
|
+
* agent's `send` with a provider) or `usage-limit` (the button on the
|
|
17
|
+
* usage-limit note).
|
|
18
|
+
*
|
|
19
|
+
* Additive and idempotent. Safe to run on every boot.
|
|
20
|
+
*
|
|
21
|
+
* Usage: node server/migrations/0032-runtime-session-billing-switch.mjs [path/to/copilot.db]
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { fileURLToPath } from 'url';
|
|
25
|
+
import path from 'path';
|
|
26
|
+
import process from 'process';
|
|
27
|
+
import Database from 'better-sqlite3';
|
|
28
|
+
|
|
29
|
+
// Added to runtime_sessions, with their DDL.
|
|
30
|
+
export const RUNTIME_SESSION_BILLING_SWITCH_COLUMNS = Object.freeze([
|
|
31
|
+
['billing_switched_at', 'TEXT'],
|
|
32
|
+
['billing_switched_by', 'TEXT'],
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
function tableExists(db, name) {
|
|
36
|
+
return !!db
|
|
37
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
38
|
+
.get(name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function columnNames(db, table) {
|
|
42
|
+
return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((column) => column.name));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** What keeps the switch from recording itself: '' when nothing does. */
|
|
46
|
+
export function runtimeSessionBillingSwitchProblem(db) {
|
|
47
|
+
if (!tableExists(db, 'runtime_sessions')) return 'no runtime_sessions table';
|
|
48
|
+
const present = columnNames(db, 'runtime_sessions');
|
|
49
|
+
const missing = RUNTIME_SESSION_BILLING_SWITCH_COLUMNS.map(([name]) => name).filter((name) => !present.has(name));
|
|
50
|
+
if (missing.length) return `runtime_sessions lacks ${missing.join(', ')}`;
|
|
51
|
+
return '';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function migrateRuntimeSessionBillingSwitch(db) {
|
|
55
|
+
if (!tableExists(db, 'runtime_sessions')) return { applied: false, columnsAdded: 0 };
|
|
56
|
+
let columnsAdded = 0;
|
|
57
|
+
const present = columnNames(db, 'runtime_sessions');
|
|
58
|
+
for (const [name, ddl] of RUNTIME_SESSION_BILLING_SWITCH_COLUMNS) {
|
|
59
|
+
if (present.has(name)) continue;
|
|
60
|
+
db.exec(`ALTER TABLE runtime_sessions ADD COLUMN ${name} ${ddl}`);
|
|
61
|
+
columnsAdded += 1;
|
|
62
|
+
}
|
|
63
|
+
return { applied: columnsAdded > 0, columnsAdded };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function defaultDbPath() {
|
|
67
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
68
|
+
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function migrate(dbPath, options = {}) {
|
|
72
|
+
const db = new Database(dbPath, options);
|
|
73
|
+
try {
|
|
74
|
+
return migrateRuntimeSessionBillingSwitch(db);
|
|
75
|
+
} finally {
|
|
76
|
+
db.close();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
81
|
+
if (isMain) {
|
|
82
|
+
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
83
|
+
try {
|
|
84
|
+
const result = migrate(dbPath);
|
|
85
|
+
console.log(`Migration 0032 on ${dbPath}`);
|
|
86
|
+
console.log(result.applied
|
|
87
|
+
? ` runtime session billing switch: columns added ${result.columnsAdded}`
|
|
88
|
+
: ' skipped: nothing to change');
|
|
89
|
+
process.exit(0);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error(`Migration 0032 failed: ${err?.message || err}`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration: Claude API usage checkpoints (services/plan-usage-service.mjs,
|
|
5
|
+
* recordClaudeApiUsageReport; the booking behind the Claude API card and its
|
|
6
|
+
* monthly cap).
|
|
7
|
+
*
|
|
8
|
+
* Both Claude workers, the subscription one and the Claude API one, post
|
|
9
|
+
* after every turn (POST /api/claude-api-usage). A chat can move between the
|
|
10
|
+
* two billings and keeps its Claude Code session, so what one billing ran
|
|
11
|
+
* shows up in the totals the other one reports after the switch. One row per
|
|
12
|
+
* (conversation, Claude Code session, CLI process, model):
|
|
13
|
+
*
|
|
14
|
+
* - `conversation_id`, `native_session_id` (the CLI's session id), `run_id`
|
|
15
|
+
* (the id the worker gives each CLI process it spawns), `model`.
|
|
16
|
+
* - `billing`: the kind of the worker that posted: `claude` or `claude-api`.
|
|
17
|
+
* - `run_hit_tokens`, `run_miss_tokens`, `run_output_tokens`, `run_cost_usd`:
|
|
18
|
+
* what this process has run so far, counted from zero, as far as it was
|
|
19
|
+
* booked. A post books its rise over these; one at or below them is a
|
|
20
|
+
* retried or reordered post and books nothing. Only `claude-api` rows book
|
|
21
|
+
* spend.
|
|
22
|
+
* - `session_hit_tokens`, `session_miss_tokens`, `session_output_tokens`,
|
|
23
|
+
* `session_cost_usd`: the CLI's own cumulative totals for the session at
|
|
24
|
+
* that post. The row of a session with the latest `last_turn_at` is the
|
|
25
|
+
* session's mark: a CLI resumed on the session, under either billing,
|
|
26
|
+
* re-reports the earlier turns in its first result, and the mark is what
|
|
27
|
+
* that first result is measured against.
|
|
28
|
+
* - `cost_unknown`: 1 when the CLI had no price for the model.
|
|
29
|
+
* - `last_turn_id` (the queue row of the turn), `last_turn_at` (when its
|
|
30
|
+
* result came): the turn the row covers. A mark is used only when no
|
|
31
|
+
* completed turn of the session is newer than it.
|
|
32
|
+
* - `updated_at`.
|
|
33
|
+
*
|
|
34
|
+
* Additive and idempotent. Safe to run on every boot.
|
|
35
|
+
*
|
|
36
|
+
* Usage: node server/migrations/0033-claude-api-usage-checkpoints.mjs [path/to/copilot.db]
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { fileURLToPath } from 'url';
|
|
40
|
+
import path from 'path';
|
|
41
|
+
import process from 'process';
|
|
42
|
+
import Database from 'better-sqlite3';
|
|
43
|
+
|
|
44
|
+
export const CLAUDE_API_USAGE_CHECKPOINTS_TABLE = 'claude_api_usage_checkpoints';
|
|
45
|
+
|
|
46
|
+
export const CLAUDE_API_USAGE_CHECKPOINT_COLUMNS = Object.freeze([
|
|
47
|
+
'conversation_id',
|
|
48
|
+
'native_session_id',
|
|
49
|
+
'run_id',
|
|
50
|
+
'model',
|
|
51
|
+
'billing',
|
|
52
|
+
'run_hit_tokens',
|
|
53
|
+
'run_miss_tokens',
|
|
54
|
+
'run_output_tokens',
|
|
55
|
+
'run_cost_usd',
|
|
56
|
+
'session_hit_tokens',
|
|
57
|
+
'session_miss_tokens',
|
|
58
|
+
'session_output_tokens',
|
|
59
|
+
'session_cost_usd',
|
|
60
|
+
'cost_unknown',
|
|
61
|
+
'last_turn_id',
|
|
62
|
+
'last_turn_at',
|
|
63
|
+
'updated_at',
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
function tableExists(db, name) {
|
|
67
|
+
return !!db
|
|
68
|
+
.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
69
|
+
.get(name);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** What keeps the service from using the table: '' when nothing does. */
|
|
73
|
+
export function claudeApiUsageCheckpointsProblem(db) {
|
|
74
|
+
if (!tableExists(db, CLAUDE_API_USAGE_CHECKPOINTS_TABLE)) return `no ${CLAUDE_API_USAGE_CHECKPOINTS_TABLE} table`;
|
|
75
|
+
const present = new Set(db.prepare(`PRAGMA table_info(${CLAUDE_API_USAGE_CHECKPOINTS_TABLE})`).all().map((column) => column.name));
|
|
76
|
+
const missing = CLAUDE_API_USAGE_CHECKPOINT_COLUMNS.filter((name) => !present.has(name));
|
|
77
|
+
if (missing.length) return `${CLAUDE_API_USAGE_CHECKPOINTS_TABLE} lacks ${missing.join(', ')}`;
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function migrateClaudeApiUsageCheckpoints(db) {
|
|
82
|
+
if (tableExists(db, CLAUDE_API_USAGE_CHECKPOINTS_TABLE)) return { applied: false, tablesAdded: 0 };
|
|
83
|
+
db.exec(`
|
|
84
|
+
CREATE TABLE ${CLAUDE_API_USAGE_CHECKPOINTS_TABLE} (
|
|
85
|
+
conversation_id TEXT NOT NULL,
|
|
86
|
+
native_session_id TEXT NOT NULL,
|
|
87
|
+
run_id TEXT NOT NULL,
|
|
88
|
+
model TEXT NOT NULL,
|
|
89
|
+
billing TEXT NOT NULL,
|
|
90
|
+
run_hit_tokens INTEGER NOT NULL DEFAULT 0,
|
|
91
|
+
run_miss_tokens INTEGER NOT NULL DEFAULT 0,
|
|
92
|
+
run_output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
93
|
+
run_cost_usd REAL NOT NULL DEFAULT 0,
|
|
94
|
+
session_hit_tokens INTEGER NOT NULL DEFAULT 0,
|
|
95
|
+
session_miss_tokens INTEGER NOT NULL DEFAULT 0,
|
|
96
|
+
session_output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
97
|
+
session_cost_usd REAL NOT NULL DEFAULT 0,
|
|
98
|
+
cost_unknown INTEGER NOT NULL DEFAULT 0,
|
|
99
|
+
last_turn_id TEXT,
|
|
100
|
+
last_turn_at TEXT,
|
|
101
|
+
updated_at TEXT NOT NULL,
|
|
102
|
+
PRIMARY KEY (conversation_id, native_session_id, run_id, model)
|
|
103
|
+
)
|
|
104
|
+
`);
|
|
105
|
+
db.exec(`
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_claude_api_usage_checkpoints_mark
|
|
107
|
+
ON ${CLAUDE_API_USAGE_CHECKPOINTS_TABLE}(native_session_id, last_turn_at)
|
|
108
|
+
`);
|
|
109
|
+
return { applied: true, tablesAdded: 1 };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function defaultDbPath() {
|
|
113
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
114
|
+
return path.resolve(here, '..', 'data', 'copilot.db');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function migrate(dbPath, options = {}) {
|
|
118
|
+
const db = new Database(dbPath, options);
|
|
119
|
+
try {
|
|
120
|
+
return migrateClaudeApiUsageCheckpoints(db);
|
|
121
|
+
} finally {
|
|
122
|
+
db.close();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
127
|
+
if (isMain) {
|
|
128
|
+
const dbPath = process.argv[2] ? path.resolve(process.argv[2]) : defaultDbPath();
|
|
129
|
+
try {
|
|
130
|
+
const result = migrate(dbPath);
|
|
131
|
+
console.log(`Migration 0033 on ${dbPath}`);
|
|
132
|
+
console.log(result.applied ? ` claude api usage checkpoints: tables added ${result.tablesAdded}` : ' skipped: nothing to change');
|
|
133
|
+
process.exit(0);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error(`Migration 0033 failed: ${err?.message || err}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|