@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
package/server/relay-tools.md
CHANGED
|
@@ -23,7 +23,7 @@ Use `restart: true` in the request body when the user wants a real relay restart
|
|
|
23
23
|
|
|
24
24
|
Never pass `immediate` in that request body: it restarts without waiting for the running turns, which only the user may decide, with Restart now in the web UI.
|
|
25
25
|
|
|
26
|
-
To go on with your work once the relay is back, subscribe first with `
|
|
26
|
+
To go on with your work once the relay is back, subscribe first with `relay_tool` `{"action":"subscribe","events":["relay_restarted"],"once":true}` (no sessions needed), then request the restart. Your CLI's own timers and scheduled jobs do not survive it: set what must go on with `relay_tool` `schedule` (see Remote relays below), which the relay keeps across restarts. A restart requested while a turn that `relay_restarted` woke is still running does not wake that conversation (a `once` subscription ends with it).
|
|
27
27
|
|
|
28
28
|
## Preview servers
|
|
29
29
|
|
|
@@ -33,7 +33,7 @@ If you have a `preview` tool, use it. Otherwise use the authenticated localhost
|
|
|
33
33
|
|
|
34
34
|
## Remote relays
|
|
35
35
|
|
|
36
|
-
If you have a `
|
|
36
|
+
If you have a `relay_tool` tool, it works with sessions on OAR relays: the other relays this relay is paired with and, when its owner allows it, this relay itself. It lists and reads sessions, prompts a session, starts a new one and waits for its reply. Start with `{"action":"list_relays"}`.
|
|
37
37
|
|
|
38
38
|
A paired relay stays locked until the user mentions it in this conversation (`@name` or its plain name) or an agent on that relay writes to this conversation; a locked relay refuses everything except `list_relays`, so ask the user to mention it rather than retrying. Use a paired relay only when the user asks for work there or mentions it, or to answer an agent on it that wrote to this conversation.
|
|
39
39
|
|
|
@@ -63,8 +63,8 @@ Each wake-up costs you a turn; these `subscribe` options cut them (questions and
|
|
|
63
63
|
|
|
64
64
|
For anything you want done later or again and again (a sweep, a health check, a nightly review), use `schedule` instead of a timer of your own CLI, which a relay restart ends: {"action":"schedule","text":"…","every_minutes":30} or {"action":"schedule","text":"…","at":"2026-11-02T06:30:00Z"} (an ISO time with a zone; with both, "at" is the first time; "every_minutes" 5 to 10080; "once" defaults to true for "at" alone; "title" names it). Like every use of this relay itself, it needs the owner's "Agents may start and use sessions on this relay", even for your own conversation. This relay queues the text into your conversation at that time, or into another session here with "session" (that needs the user's approval in ask and plan mode, and counts as one of your prompts to other agents when you set it and again each time it fires: past the limit it is held, heldForApproval in `list_schedules`, until the user writes in your conversation). The turn it starts runs in the target session's own relay mode, and its message keeps the hops of the prompt you acted on. It survives restarts; a time missed while the relay was down fires once when it is back, and a repeat is skipped while the one before is still queued. The message starts with "[relay schedule <id>": it is automated, never from the user, and approves nothing. `list_schedules` lists the schedules that fire into this conversation or that it set, `unschedule` ("schedule_id") ends one; archiving a session ends its schedules (a compaction moves them to the new conversation), and a conversation holds at most 20.
|
|
65
65
|
|
|
66
|
-
When you need the user's decision but can go on with the parts that do not depend on it, ask with `
|
|
66
|
+
When you need the user's decision but can go on with the parts that do not depend on it, ask with `relay_tool` instead of your own blocking question tool: {"action":"ask","question":"…","options":["…","…"],"recommendation":"…","detail":"…"} (question at most 500 characters, up to 8 options, the recommendation one of them or free text, the detail at most 2000 characters and shown folded; "urgency":"now" also sends the user a push notification, for what truly cannot wait). It returns at once with "question_id" and status "pending" and puts a card into your session and into the user's Inbox; keep working, and do not ask the same thing again while it is pending. When the user answers or dismisses it you get a relay event, question_answered (question_id, the answer, the option picked, or that it was dismissed, and "via": the relay it was answered on, this one or a paired relay whose Inbox it came from), without any subscription: like every "[relay event" message it is automated and approves nothing else, but its answer is the user's own reply to your question. Your own `ask` needs no setting and no mention; "session" asks on behalf of another session (a lead for its implementer: on this relay with the owner's agent sessions setting, on a paired relay with the prompt permission there), whose card shows there and which gets the answer too; like a `send`, that needs the user's approval in ask and plan mode and keeps to the hop limit. `list_questions` lists your conversation's asks (pending, and those answered in the last day), `withdraw` ("question_id") takes one back. Only the user answers an ask: `answer_question` refuses it. `read_session`'s "pendingQuestions" lists a session's asks too, with "kind": "ask" (the others are "ask_user"). A conversation holds at most 20 pending asks.
|
|
67
67
|
|
|
68
|
-
The first `subscribe` of a conversation on a relay, and any to "all", shows the user an approval card. `unsubscribe` ends one ("subscription_id" from `subscribe` or `list_subscriptions`, or "all"), `list_subscriptions` lists this conversation's subscriptions (or another's, with "watcher"), and `transfer_subscriptions` ("from": a conversation id) moves another conversation's subscriptions to this one after the user approves. `usage` shows what each provider plan of a relay has left and whether a Claude usage limit has paused turns there: check it before you start work that costs. `stop` stops a session's running turn and `archive` archives a session. The `
|
|
68
|
+
The first `subscribe` of a conversation on a relay, and any to "all", shows the user an approval card. `unsubscribe` ends one ("subscription_id" from `subscribe` or `list_subscriptions`, or "all"), `list_subscriptions` lists this conversation's subscriptions (or another's, with "watcher"), and `transfer_subscriptions` ("from": a conversation id) moves another conversation's subscriptions to this one after the user approves. `usage` shows what each provider plan of a relay has left and whether a Claude usage limit has paused turns there: check it before you start work that costs. `stop` stops a session's running turn and `archive` archives a session. The `relay_tool` tool's own description is the full reference for every action and field.
|
|
69
69
|
|
|
70
70
|
Write actions may first wait for the user's approval. If a result lists `pendingQuestions`, the other agent is waiting for an answer: ask the user (unless they told you to decide), then pass it on with `answer_question`. Always tell the user which relay and session you worked on.
|
|
@@ -7,7 +7,7 @@ import { compareGroupMembers } from '../public/app/conversation-group-order.mjs'
|
|
|
7
7
|
// Session groups (migration 0014): the set of conversations that share a
|
|
8
8
|
// `group_id`, with a row in `conversation_groups` for what the set itself
|
|
9
9
|
// needs: a name given by hand, and the head (the session that created the
|
|
10
|
-
// others with the
|
|
10
|
+
// others with the relay_tool; NULL for a group made by hand or once
|
|
11
11
|
// the head is deleted).
|
|
12
12
|
//
|
|
13
13
|
// A session an agent creates on this relay joins its creator's group at
|
|
@@ -18,7 +18,9 @@ import { compareGroupMembers } from '../public/app/conversation-group-order.mjs'
|
|
|
18
18
|
//
|
|
19
19
|
// A group can have a hand order (migration 0015): once the owner drags a row
|
|
20
20
|
// into place, every member after the head gets a `group_position` (10, 20,
|
|
21
|
-
// 30, ...) and keeps it; later
|
|
21
|
+
// 30, ...) and keeps it; a later arrival (a session created in the group, a
|
|
22
|
+
// row the menu moves in) goes first, right under the head, and the next
|
|
23
|
+
// drag renumbers from 10 again. A group nobody
|
|
22
24
|
// reordered keeps NULL positions and the activity order. Without the column
|
|
23
25
|
// the hand order is off and moves only change the membership.
|
|
24
26
|
//
|
|
@@ -141,8 +143,8 @@ export function createConversationGroupRepository(db) {
|
|
|
141
143
|
WHERE group_id = ? AND status != 'deleted'
|
|
142
144
|
ORDER BY updated_at DESC
|
|
143
145
|
`);
|
|
144
|
-
const
|
|
145
|
-
? db.prepare(`SELECT
|
|
146
|
+
const minPosition = hasPosition
|
|
147
|
+
? db.prepare(`SELECT MIN(group_position) AS n FROM conversations WHERE group_id = ? AND status != 'deleted'`)
|
|
146
148
|
: null;
|
|
147
149
|
const countMembers = db.prepare(`SELECT COUNT(*) AS n FROM conversations WHERE group_id = ? AND status != 'deleted'`);
|
|
148
150
|
// Groups with a live member, the one with the latest activity first, then
|
|
@@ -188,11 +190,16 @@ export function createConversationGroupRepository(db) {
|
|
|
188
190
|
|
|
189
191
|
const positionValue = (row) => (row?.group_position == null ? null : Number(row.group_position));
|
|
190
192
|
|
|
191
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* Where a row joining `groupId` goes: before the first placed member (the
|
|
195
|
+
* owner sees a new session at the top of its group, under the head), or
|
|
196
|
+
* NULL when nobody ordered the group, where newest-first puts it on top
|
|
197
|
+
* anyway. Positions may go to zero and below; the next drag renumbers.
|
|
198
|
+
*/
|
|
192
199
|
function joiningPosition(groupId) {
|
|
193
200
|
if (!hasPosition) return null;
|
|
194
|
-
const
|
|
195
|
-
return
|
|
201
|
+
const min = minPosition.get(toText(groupId))?.n;
|
|
202
|
+
return min == null ? null : Number(min) - POSITION_STEP;
|
|
196
203
|
}
|
|
197
204
|
|
|
198
205
|
const liveConversation = (id) => {
|
|
@@ -15,6 +15,8 @@ export function createMessageRepository(db) {
|
|
|
15
15
|
// worker, so it neither keeps one warm nor counts as queued.
|
|
16
16
|
const queueHasUsageLimitPause = db.prepare(`PRAGMA table_info(queue)`).all()
|
|
17
17
|
.some((column) => column.name === 'usage_limit_pause');
|
|
18
|
+
const queueHasProvider = db.prepare(`PRAGMA table_info(queue)`).all()
|
|
19
|
+
.some((column) => column.name === 'provider');
|
|
18
20
|
const notHeldForUsageLimit = (alias = '') => (queueHasUsageLimitPause
|
|
19
21
|
? `AND NOT (${alias}usage_limit_pause IS NOT NULL AND ${alias}status = 'pending' AND ${alias}next_attempt_at IS NOT NULL AND ${alias}next_attempt_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`
|
|
20
22
|
: '');
|
|
@@ -351,6 +353,11 @@ export function createMessageRepository(db) {
|
|
|
351
353
|
WHERE id = ?
|
|
352
354
|
`),
|
|
353
355
|
setQueueRuntimeSession: db.prepare(`UPDATE queue SET runtime_session_id = ? WHERE id = ?`),
|
|
356
|
+
// The billing a row runs on (migration 0031): a continuation is the
|
|
357
|
+
// kind of the worker whose CLI started it, whatever the chat's now.
|
|
358
|
+
setQueueProvider: queueHasProvider
|
|
359
|
+
? db.prepare(`UPDATE queue SET provider = ? WHERE id = ?`)
|
|
360
|
+
: null,
|
|
354
361
|
setQueueResponseMessageId: db.prepare(`UPDATE queue SET response_message_id = ? WHERE id = ?`),
|
|
355
362
|
// Legacy (unfenced) finalization keeps 'pending' in the set so a
|
|
356
363
|
// response landing after a stale-sweep requeue still finalizes; fenced
|
|
@@ -11,6 +11,12 @@ export function createSessionRepository(db) {
|
|
|
11
11
|
: 'rs.model AS runtime_model, NULL AS runtime_provider_type, NULL AS runtime_provider_model';
|
|
12
12
|
// Provenance of conversations another relay's agent created (migration
|
|
13
13
|
// 0005); NULL on a schema that predates it.
|
|
14
|
+
// The billing a queue row runs on (migration 0031): the Claude API key
|
|
15
|
+
// removal counts the unfinished rows stamped with the key.
|
|
16
|
+
const queueHasProvider = db.prepare(`PRAGMA table_info(queue)`).all().some((column) => column.name === 'provider');
|
|
17
|
+
const claudeApiQueueCountSelect = queueHasProvider
|
|
18
|
+
? `(SELECT COUNT(*) FROM queue q WHERE q.conversation_id = c.id AND q.status IN ('pending', 'processing', 'parked') AND LOWER(COALESCE(q.provider, '')) = 'claude-api') AS claude_api_queue_count`
|
|
19
|
+
: '0 AS claude_api_queue_count';
|
|
14
20
|
const conversationsHaveOrigin = db.prepare(`PRAGMA table_info(conversations)`).all()
|
|
15
21
|
.some((column) => String(column?.name || '').trim() === 'origin_json');
|
|
16
22
|
const conversationOriginSelect = conversationsHaveOrigin ? 'c.origin_json' : 'NULL AS origin_json';
|
|
@@ -100,7 +106,8 @@ export function createSessionRepository(db) {
|
|
|
100
106
|
rs.*,
|
|
101
107
|
c.sdk_session_id AS conversation_sdk_session_id,
|
|
102
108
|
(SELECT COUNT(*) FROM messages m WHERE m.conversation_id = c.id) AS message_count,
|
|
103
|
-
(SELECT COUNT(*) FROM queue q WHERE q.conversation_id = c.id AND q.status IN ('pending', 'processing', 'parked')) AS active_queue_count
|
|
109
|
+
(SELECT COUNT(*) FROM queue q WHERE q.conversation_id = c.id AND q.status IN ('pending', 'processing', 'parked')) AS active_queue_count,
|
|
110
|
+
${claudeApiQueueCountSelect}
|
|
104
111
|
FROM runtime_sessions rs
|
|
105
112
|
JOIN conversations c ON c.id = rs.conversation_id
|
|
106
113
|
WHERE c.status != 'deleted'
|
|
@@ -192,7 +192,7 @@ export function registerAskUserRoutes(app, deps) {
|
|
|
192
192
|
});
|
|
193
193
|
|
|
194
194
|
/**
|
|
195
|
-
* An ask (
|
|
195
|
+
* An ask (relay_tool `ask`) is the user's to answer: from this relay's
|
|
196
196
|
* own UI here, from a paired relay's Inbox through its own route
|
|
197
197
|
* (relay-ask-routes.mjs). Never from an agent, this relay's or another's.
|
|
198
198
|
*/
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// (messages-routes.mjs), next to the code they reuse.
|
|
8
8
|
//
|
|
9
9
|
// The owner's action only: every route is behind `auth`, and a request that
|
|
10
|
-
// arrived from a paired relay is refused, as the
|
|
10
|
+
// arrived from a paired relay is refused, as the relay_tool's own
|
|
11
11
|
// route refuses calls from other relays. The tool reads groups, names an
|
|
12
12
|
// existing one for a session it creates (the bootstrap route), and moves one
|
|
13
13
|
// session into a group, out of it, or into a new group of a name it gives
|
|
@@ -129,7 +129,8 @@ export function registerConversationGroupRoutes(app, deps) {
|
|
|
129
129
|
// a conversation into a group. `groupId: null` takes it out (as DELETE).
|
|
130
130
|
// With `beforeConversationId` in the body (null: to the end) it is a drag:
|
|
131
131
|
// the row lands before that one and the group gets a hand order; without
|
|
132
|
-
// it the row joins
|
|
132
|
+
// it the row joins a hand-ordered group first, under the head, as the menu
|
|
133
|
+
// does (and the next drag renumbers).
|
|
133
134
|
app.put('/api/conversation/:id/group', auth, (req, res) => {
|
|
134
135
|
if (refuseRemote(req, res)) return;
|
|
135
136
|
if (!conversationGroups?.enabled) return unavailable(res);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// POST /api/deepseek-usage-import: the two CSV files of DeepSeek's usage
|
|
4
|
+
// export, as text, from Settings → Providers → DeepSeek. The relay's user
|
|
5
|
+
// only: another relay's agent gets a 403. The import itself is in
|
|
6
|
+
// services/deepseek-usage-import.mjs.
|
|
7
|
+
|
|
8
|
+
import { readRemoteRelayRequest } from '../services/remote-relay-inbound.mjs';
|
|
9
|
+
|
|
10
|
+
// The two files of a month come to well under a megabyte.
|
|
11
|
+
export const DEEPSEEK_USAGE_IMPORT_MAX_BYTES = 2 * 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
// How long the import waits for the usage card's currency (the balance's).
|
|
14
|
+
export const DEEPSEEK_USAGE_IMPORT_CURRENCY_WAIT_MS = 5000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} deps
|
|
18
|
+
* @param {Function} [deps.getCardCurrency] the usage card's currency ('USD',
|
|
19
|
+
* 'CNY'), from the balance; may be async. The summary then says whether
|
|
20
|
+
* the card can use the export's rows.
|
|
21
|
+
* @param {Function} [deps.logError] where a failed import is logged
|
|
22
|
+
*/
|
|
23
|
+
export function registerDeepSeekUsageImportRoutes(app, deps) {
|
|
24
|
+
const {
|
|
25
|
+
auth,
|
|
26
|
+
deepseekUsageImportService,
|
|
27
|
+
remoteRelayInbound,
|
|
28
|
+
getCardCurrency = null,
|
|
29
|
+
logError = (message, err) => console.error(message, err),
|
|
30
|
+
} = deps;
|
|
31
|
+
|
|
32
|
+
async function cardCurrency() {
|
|
33
|
+
if (typeof getCardCurrency !== 'function') return null;
|
|
34
|
+
let timer = null;
|
|
35
|
+
try {
|
|
36
|
+
return await Promise.race([
|
|
37
|
+
Promise.resolve().then(() => getCardCurrency()),
|
|
38
|
+
new Promise((resolve) => { timer = setTimeout(() => resolve(null), DEEPSEEK_USAGE_IMPORT_CURRENCY_WAIT_MS); }),
|
|
39
|
+
]);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
} finally {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
app.post('/api/deepseek-usage-import', auth, async (req, res) => {
|
|
48
|
+
if (readRemoteRelayRequest(req, remoteRelayInbound || {}).remote) {
|
|
49
|
+
return res.status(403).json({ error: 'The DeepSeek usage export is imported by this relay\'s user only.' });
|
|
50
|
+
}
|
|
51
|
+
if (typeof deepseekUsageImportService?.importExport !== 'function') {
|
|
52
|
+
return res.status(501).json({ error: 'The DeepSeek usage import is not available on this relay.' });
|
|
53
|
+
}
|
|
54
|
+
const amount = req.body?.amount;
|
|
55
|
+
const cost = req.body?.cost;
|
|
56
|
+
if (typeof amount !== 'string' || typeof cost !== 'string') {
|
|
57
|
+
return res.status(400).json({ error: 'Choose both files of the export: the amount file and the cost file.' });
|
|
58
|
+
}
|
|
59
|
+
if (Buffer.byteLength(amount, 'utf8') + Buffer.byteLength(cost, 'utf8') > DEEPSEEK_USAGE_IMPORT_MAX_BYTES) {
|
|
60
|
+
return res.status(413).json({ error: 'The files are larger than 2 MB together; export a shorter period.' });
|
|
61
|
+
}
|
|
62
|
+
let result;
|
|
63
|
+
try {
|
|
64
|
+
result = deepseekUsageImportService.importExport({ amount, cost, cardCurrency: await cardCurrency() });
|
|
65
|
+
} catch (err) {
|
|
66
|
+
logError('[deepseek-usage-import] the import failed:', err);
|
|
67
|
+
return res.status(500).json({ error: 'The import failed on the relay; nothing was imported. Its log has the reason.' });
|
|
68
|
+
}
|
|
69
|
+
if (!result?.ok) return res.status(400).json({ error: result?.error || 'The export could not be read.' });
|
|
70
|
+
res.json({ ok: true, summary: result.summary });
|
|
71
|
+
});
|
|
72
|
+
}
|