@oar-sh/oar 0.9.18 → 0.9.20
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 +2 -26
- package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +13 -1
- package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +2 -26
- package/CHANGELOG.md +359 -0
- package/README.md +10 -10
- package/bin/oar.js +391 -97
- package/package.json +4 -2
- package/server/claude-cloud-worker/claude-cloud-event-normalizer.mjs +60 -0
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +27 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +3 -2
- package/server/claude-worker/claude-session-process.mjs +3 -0
- package/server/claude-worker/claude-turn-publisher.mjs +163 -8
- package/server/claude-worker/claude-usage-totals.mjs +58 -0
- package/server/copilot-worker/copilot-plan-board.mjs +4 -7
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +86 -3
- package/server/copilot-worker/copilot-sdk-session-process.mjs +57 -4
- package/server/cursor-worker/cursor-turn-runner.mjs +42 -6
- package/server/db-schema.mjs +18 -0
- package/server/deepseek-worker/deepseek-claude-backend.mjs +14 -0
- package/server/grok-worker/grok-turn-runner.mjs +44 -6
- package/server/migrations/0001-add-structured-answer.mjs +5 -7
- package/server/migrations/0002-recent-workspace-roots-path-key.mjs +5 -7
- package/server/migrations/0003-push-subscriptions.mjs +5 -7
- package/server/migrations/0004-steer-settle-markers.mjs +5 -7
- package/server/migrations/0005-remote-relays.mjs +5 -7
- package/server/migrations/0006-workspace-root-settings.mjs +5 -7
- package/server/migrations/0007-claude-cloud.mjs +5 -7
- package/server/migrations/0008-message-pins.mjs +5 -7
- package/server/migrations/0009-subagent-timeline.mjs +5 -7
- package/server/migrations/0010-subagent-timeline-pruning.mjs +5 -7
- package/server/migrations/0011-relay-event-subscriptions.mjs +5 -7
- package/server/migrations/0012-cross-relay-event-subscriptions.mjs +5 -7
- package/server/migrations/0013-previews.mjs +5 -7
- package/server/migrations/0014-conversation-groups.mjs +5 -7
- package/server/migrations/0015-conversation-group-position.mjs +5 -7
- package/server/migrations/0016-share-access-log.mjs +5 -7
- package/server/migrations/0017-conversation-history-indexes.mjs +5 -7
- package/server/migrations/0018-background-task-rows.mjs +5 -7
- package/server/migrations/0019-relay-event-subscription-options.mjs +5 -7
- package/server/migrations/0020-conversation-may-create-sessions.mjs +5 -7
- package/server/migrations/0021-conversation-group-agent-marks.mjs +5 -7
- package/server/migrations/0022-relay-schedules.mjs +5 -7
- package/server/migrations/0023-relay-asks.mjs +5 -7
- package/server/migrations/0024-deepseek-usage-totals.mjs +5 -7
- package/server/migrations/0025-deepseek-usage-requests-daily.mjs +5 -7
- package/server/migrations/0026-deepseek-usage-served-models.mjs +5 -7
- package/server/migrations/0027-deepseek-usage-checkpoint-runs.mjs +5 -7
- package/server/migrations/0028-deepseek-usage-checkpoint-request-runs.mjs +5 -7
- package/server/migrations/0029-deepseek-balance-rises.mjs +5 -7
- package/server/migrations/0030-deepseek-usage-actuals.mjs +5 -7
- package/server/migrations/0031-queue-provider.mjs +5 -7
- package/server/migrations/0032-runtime-session-billing-switch.mjs +5 -7
- package/server/migrations/0033-claude-api-usage-checkpoints.mjs +5 -7
- package/server/migrations/0034-claude-api-usage-totals.mjs +5 -7
- package/server/migrations/0035-conversation-plan-only.mjs +87 -0
- package/server/migrations/0036-token-usage.mjs +267 -0
- package/server/public/app/about-view.mjs +87 -3
- package/server/public/app/api-client.js +2 -1
- package/server/public/app/bootstrap.js +55 -2
- package/server/public/app/composer-control-state.mjs +12 -2
- package/server/public/app/context-usage-view.mjs +16 -0
- package/server/public/app/conversation-view.js +17 -2
- package/server/public/app/inbox-ui.js +32 -0
- package/server/public/app/inbox-view.mjs +141 -5
- package/server/public/app/plan-board-shared.mjs +49 -0
- package/server/public/app/plan-usage-view.mjs +1 -1
- package/server/public/app/relay-board-inline.mjs +4 -1
- package/server/public/app/relay-board-view.js +38 -10
- package/server/public/app/relay-event-view.mjs +4 -0
- package/server/public/app/socket-handlers.js +10 -2
- package/server/public/index.html +14 -0
- package/server/public/sw.js +8 -0
- package/server/relay-heap-guard.mjs +351 -0
- package/server/relay-self-restart.mjs +116 -9
- package/server/relay-tools.md +5 -4
- package/server/repositories/message-repository.mjs +31 -1
- package/server/repositories/remote-relay-repository.mjs +10 -0
- package/server/repositories/session-repository.mjs +6 -1
- package/server/routes/messages-routes.mjs +120 -4
- package/server/routes/relay-ask-routes.mjs +138 -8
- package/server/routes/relay-board-routes.mjs +55 -40
- package/server/routes/sessions-routes.mjs +141 -31
- package/server/server-runtime.mjs +142 -11
- package/server/services/about-service.mjs +59 -0
- package/server/services/cache-rebuild-service.mjs +7 -0
- package/server/services/conversation-preferences-service.mjs +31 -0
- package/server/services/database-file.mjs +413 -0
- package/server/services/db-backup-service.mjs +809 -0
- package/server/services/deepseek-usage-import.mjs +6 -9
- package/server/services/delete-archive-service.mjs +3 -0
- package/server/services/earlier-database-service.mjs +69 -126
- package/server/services/oar-cli-helpers.mjs +214 -12
- package/server/services/oar-cli-menu.mjs +1 -0
- package/server/services/oar-state-migration-service.mjs +33 -13
- package/server/services/package-data-rescue.mjs +4 -2
- package/server/services/plan-usage-claude-api.mjs +14 -15
- package/server/services/plan-usage-contract.mjs +6 -2
- package/server/services/plan-usage-deepseek.mjs +1 -1
- package/server/services/plan-usage-tokens.mjs +248 -0
- package/server/services/push-dispatch-service.mjs +134 -9
- package/server/services/relay-cli-launcher-service.mjs +3 -0
- package/server/services/relay-event-service.mjs +30 -0
- package/server/services/relay-question-context.mjs +11 -0
- package/server/services/relay-restart-picture.mjs +67 -22
- package/server/services/remote-inbox-watch-service.mjs +204 -0
- package/server/services/remote-relay-caller-context.mjs +80 -7
- package/server/services/remote-relay-dispatcher.mjs +68 -8
- package/server/services/restart-resume-service.mjs +5 -2
- package/server/services/session-worker-launch-service.mjs +12 -3
- package/server/services/session-worker-stop-service.mjs +27 -6
- package/server/services/token-usage-service.mjs +430 -0
- package/server/services/update-install-service.mjs +13 -0
- package/server/services/usage-limit-pause-service.mjs +8 -1
- package/server/services/worker-scope.mjs +77 -0
- package/shared/plan-board-actions.mjs +130 -0
- package/shared/plan-only.mjs +43 -0
- package/shared/relay-asks.mjs +3 -0
- package/shared/relay-events.mjs +38 -4
- package/shared/remote-relay-contract.mjs +98 -21
- package/shared/turn-token-usage.mjs +144 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { claudeBaseModelId } from './model-id.mjs';
|
|
2
|
+
|
|
3
|
+
// The token report a worker posts after a turn (POST /api/turn-token-usage),
|
|
4
|
+
// checked and normalised. Shared by the relay (the route and
|
|
5
|
+
// services/token-usage-service.mjs) and the workers that build it.
|
|
6
|
+
//
|
|
7
|
+
// A token field a provider did not report is null; a negative, non-finite
|
|
8
|
+
// or unsafely large one is not a count either and becomes null, and a
|
|
9
|
+
// fraction is cut to the integer. 0 always means the provider reported zero. `inputTokens` is fresh
|
|
10
|
+
// input: without the tokens read from or written to the prompt cache.
|
|
11
|
+
|
|
12
|
+
export const TURN_TOKEN_FIELDS = Object.freeze([
|
|
13
|
+
'inputTokens',
|
|
14
|
+
'outputTokens',
|
|
15
|
+
'cacheWriteTokens',
|
|
16
|
+
'cacheReadTokens',
|
|
17
|
+
'reasoningTokens',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export const TURN_TOKEN_COST_SOURCES = Object.freeze(['claude-code', 'provider', 'oar-deepseek-table']);
|
|
21
|
+
|
|
22
|
+
export const TURN_TOKEN_PROVIDERS = Object.freeze(['github', 'claude', 'claude-api', 'claude-cloud', 'deepseek', 'cursor', 'grok']);
|
|
23
|
+
|
|
24
|
+
// One report names at most this many models; more is not a turn.
|
|
25
|
+
export const TURN_TOKEN_MAX_MODELS = 32;
|
|
26
|
+
const ID_MAX_LENGTH = 200;
|
|
27
|
+
const BILLING_PATTERN = /^byok-[a-z0-9][a-z0-9_-]{0,39}$/;
|
|
28
|
+
|
|
29
|
+
/** A provider's model id as the tables keep it: lower case, without `[1m]`. */
|
|
30
|
+
export function tokenUsageModel(value) {
|
|
31
|
+
return claudeBaseModelId(String(value ?? '').trim()).toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Past these a value is not a count or a cost but a broken report.
|
|
35
|
+
const MAX_COST_USD = 1e9;
|
|
36
|
+
|
|
37
|
+
/** A token count, or null when the value is not one. */
|
|
38
|
+
export function tokenCount(value) {
|
|
39
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > Number.MAX_SAFE_INTEGER) return null;
|
|
40
|
+
return Math.trunc(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function costValue(value) {
|
|
44
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > MAX_COST_USD) return null;
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** a + b where either may be null: null only when both are. */
|
|
49
|
+
export function addNullable(a, b) {
|
|
50
|
+
if (a == null) return b == null ? null : b;
|
|
51
|
+
if (b == null) return a;
|
|
52
|
+
return a + b;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** '' or `byok-<provider type>`; null for anything else. */
|
|
56
|
+
export function normalizeTurnTokenBilling(value) {
|
|
57
|
+
const billing = String(value || '').trim().toLowerCase();
|
|
58
|
+
if (!billing) return '';
|
|
59
|
+
return BILLING_PATTERN.test(billing) ? billing : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* `[{ model, inputTokens, …, costUsd, costSource, costUnknown }]`, one per
|
|
64
|
+
* model: the same model named twice (also `x` and `x[1m]`) is summed.
|
|
65
|
+
*/
|
|
66
|
+
export function normalizeTurnTokenEntries(entries) {
|
|
67
|
+
const byModel = new Map();
|
|
68
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
69
|
+
const model = tokenUsageModel(entry?.model);
|
|
70
|
+
if (!model || model.length > ID_MAX_LENGTH) continue;
|
|
71
|
+
const next = {
|
|
72
|
+
model,
|
|
73
|
+
...Object.fromEntries(TURN_TOKEN_FIELDS.map((field) => [field, tokenCount(entry?.[field])])),
|
|
74
|
+
costUsd: costValue(entry?.costUsd),
|
|
75
|
+
costSource: TURN_TOKEN_COST_SOURCES.includes(entry?.costSource) ? entry.costSource : null,
|
|
76
|
+
costUnknown: entry?.costUnknown === true,
|
|
77
|
+
};
|
|
78
|
+
const current = byModel.get(model);
|
|
79
|
+
if (!current) {
|
|
80
|
+
byModel.set(model, next);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
// The caps hold for the sum too, not only for each entry.
|
|
84
|
+
for (const field of TURN_TOKEN_FIELDS) {
|
|
85
|
+
const sum = addNullable(current[field], next[field]);
|
|
86
|
+
current[field] = sum === null ? null : tokenCount(sum);
|
|
87
|
+
}
|
|
88
|
+
const costSum = addNullable(current.costUsd, next.costUsd);
|
|
89
|
+
current.costUsd = costSum === null ? null : costValue(costSum);
|
|
90
|
+
current.costSource = current.costSource || next.costSource;
|
|
91
|
+
current.costUnknown = current.costUnknown || next.costUnknown;
|
|
92
|
+
}
|
|
93
|
+
return [...byModel.values()];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function idValue(value) {
|
|
97
|
+
const id = String(value ?? '').trim();
|
|
98
|
+
return id.length > ID_MAX_LENGTH ? null : id;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The report a worker posts, checked: `{ ok, report }` or `{ ok: false,
|
|
103
|
+
* error }`. `models` carries the turn's own counts; `sessionTotals` (Claude
|
|
104
|
+
* cloud) the session's running totals, of which the relay books the growth.
|
|
105
|
+
* Who the provider and billing are is the relay's to settle; the body's are
|
|
106
|
+
* only stated.
|
|
107
|
+
*/
|
|
108
|
+
export function normalizeTurnTokenUsageReport(body = {}) {
|
|
109
|
+
const conversationId = idValue(body?.conversationId);
|
|
110
|
+
const turnId = idValue(body?.turnId);
|
|
111
|
+
const runId = idValue(body?.runId);
|
|
112
|
+
if (!conversationId) return { ok: false, error: 'Missing conversationId' };
|
|
113
|
+
if (!turnId) return { ok: false, error: 'Missing turnId' };
|
|
114
|
+
if (runId === null) return { ok: false, error: 'Invalid runId' };
|
|
115
|
+
const provider = String(body?.provider || '').trim().toLowerCase();
|
|
116
|
+
if (provider && !TURN_TOKEN_PROVIDERS.includes(provider)) return { ok: false, error: 'Unknown provider' };
|
|
117
|
+
const billing = normalizeTurnTokenBilling(body?.billing);
|
|
118
|
+
if (billing === null) return { ok: false, error: 'Invalid billing' };
|
|
119
|
+
const at = body?.at == null || body?.at === '' ? null : String(body.at).trim();
|
|
120
|
+
if (at !== null && !Number.isFinite(Date.parse(at))) return { ok: false, error: 'Invalid at' };
|
|
121
|
+
const hasModels = Array.isArray(body?.models);
|
|
122
|
+
const hasSessionTotals = Array.isArray(body?.sessionTotals);
|
|
123
|
+
if (hasModels === hasSessionTotals) return { ok: false, error: 'Send either models or sessionTotals' };
|
|
124
|
+
const raw = hasModels ? body.models : body.sessionTotals;
|
|
125
|
+
if (raw.length > TURN_TOKEN_MAX_MODELS) return { ok: false, error: `More than ${TURN_TOKEN_MAX_MODELS} models` };
|
|
126
|
+
const entries = normalizeTurnTokenEntries(raw);
|
|
127
|
+
if (!entries.length) return { ok: false, error: 'Missing models' };
|
|
128
|
+
const nativeSessionId = idValue(body?.nativeSessionId) || null;
|
|
129
|
+
if (hasSessionTotals && !nativeSessionId) return { ok: false, error: 'Missing nativeSessionId' };
|
|
130
|
+
return {
|
|
131
|
+
ok: true,
|
|
132
|
+
report: {
|
|
133
|
+
conversationId,
|
|
134
|
+
turnId,
|
|
135
|
+
runId,
|
|
136
|
+
provider,
|
|
137
|
+
billing,
|
|
138
|
+
nativeSessionId,
|
|
139
|
+
responseMessageId: idValue(body?.responseMessageId) || null,
|
|
140
|
+
at: at === null ? null : new Date(Date.parse(at)).toISOString(),
|
|
141
|
+
...(hasModels ? { models: entries } : { sessionTotals: entries }),
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|