@arcaneorion/dsh-model-channel-manager 0.3.15 → 0.4.0

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.
@@ -0,0 +1,196 @@
1
+ /** Runtime snapshots and idempotent, cancellable model tasks, independent of Config. */
2
+ import { createHash } from 'node:crypto';
3
+ import { z } from 'zod';
4
+ import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain';
5
+ import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
6
+
7
+ export const CHANNEL = '/api';
8
+ export const ENDPOINT = 'modelChannels/invoke';
9
+ const remoteInitializers = [];
10
+ /** Source-mode Gateway binding keeps the handwritten browser module build-free. */
11
+ export class ChannelController extends TypertRemoteService {
12
+ constructor(ctx, { handler }) {
13
+ super(ctx, 'modelChannelController', { namespace: 'modelChannels' });
14
+ this.handler = handler;
15
+ for (const initialize of remoteInitializers) initialize.call(this);
16
+ }
17
+ async invoke(method, payload) {
18
+ const result = await this.handler(method, payload);
19
+ if (!result.ok) throw new RemoteError(result.error.code, result.error.message, {});
20
+ return result.value;
21
+ }
22
+ }
23
+ // Standard Remote decorator semantics, expressed as JavaScript without a compiler.
24
+ Remote(ChannelController.prototype.invoke, { kind: 'method', name: 'invoke', static: false, private: false, addInitializer(fn) { remoteInitializers.push(fn); } });
25
+ export const WINDOWS = { '30m': 30 * 60000, '24h': 24 * 3600000, '7d': 7 * 24 * 3600000 };
26
+ const stateDomain = defineDomain({
27
+ name: 'model_channel_state', version: 1, layout: 'per-record',
28
+ tables: {
29
+ instances: domainTable(z.object({ runtime: z.record(z.string(), z.any()), tasks: z.array(z.any()) })),
30
+ legacy: domainTable(z.object({ health: z.any(), migratedAt: z.number() })),
31
+ },
32
+ });
33
+
34
+ /** A separate domain keeps the existing health-event format and version unchanged. */
35
+ export class RuntimeStore {
36
+ constructor(ctx, owner) {
37
+ this.ctx = ctx;
38
+ this.key = createHash('sha256').update(owner).digest('hex');
39
+ this.queue = Promise.resolve();
40
+ }
41
+ async open() {
42
+ this.domain = await this.ctx.storageDomain.open(stateDomain);
43
+ this.table = this.domain.table('instances');
44
+ this.legacy = this.domain.table('legacy');
45
+ return this.table.get(this.key);
46
+ }
47
+ save(value) {
48
+ const snapshot = structuredClone(value);
49
+ const next = this.queue.then(() => this.table.put(this.key, snapshot));
50
+ this.queue = next.catch(() => {}); // Each caller receives its own write error.
51
+ return next;
52
+ }
53
+ async archive(health) {
54
+ if (this.legacy.get(this.key) === undefined)
55
+ await this.legacy.put(this.key, { health, migratedAt: Date.now() });
56
+ }
57
+ async close() {
58
+ await this.queue;
59
+ await this.domain?.close();
60
+ }
61
+ }
62
+
63
+ /** Aggregate retained events by their actual timestamp; no last-active approximation. */
64
+ export function aggregateHealth(records, now = Date.now()) {
65
+ return Object.fromEntries(Object.entries(WINDOWS).map(([window, duration]) => {
66
+ const rows = new Map();
67
+ for (const events of Object.values(records)) for (const e of Array.isArray(events) ? events : []) {
68
+ if (!e || typeof e.provider !== 'string' || typeof e.model !== 'string' || typeof e.ok !== 'boolean' || !Number.isFinite(e.ts) || e.ts < now - duration || e.ts > now) continue;
69
+ const key = JSON.stringify([e.provider, e.model]);
70
+ let a = rows.get(key);
71
+ if (!a) {
72
+ a = { provider: e.provider, model: e.model, total: 0, success: 0, ttftSum: 0, ttftCount: 0, latSum: 0, latCount: 0, tokIn: 0, tokOut: 0, tokCache: 0, lastTs: 0, lastOk: null, lastCode: null };
73
+ rows.set(key, a);
74
+ }
75
+ a.total++;
76
+ a.success += Number(e.ok === true);
77
+ if (e.ok && Number.isFinite(e.ttftMs) && e.ttftMs >= 0) { a.ttftSum += e.ttftMs; a.ttftCount++; }
78
+ if (e.ok && Number.isFinite(e.latencyMs) && e.latencyMs >= 0) { a.latSum += e.latencyMs; a.latCount++; }
79
+ const token = n => Number.isFinite(n) && n > 0 ? n : 0;
80
+ a.tokIn += token(e.inputTokens);
81
+ a.tokOut += token(e.outputTokens);
82
+ a.tokCache += token(e.cacheReadTokens) + token(e.cacheWriteTokens);
83
+ if (e.ts >= a.lastTs) { a.lastTs = e.ts; a.lastOk = e.ok; a.lastCode = e.ok ? null : e.code || null; }
84
+ }
85
+ return [window, [...rows.values()].map(({ ttftSum, ttftCount, latSum, latCount, ...a }) => ({
86
+ ...a, ttftCount, latCount,
87
+ ttftAvg: ttftCount ? Math.round(ttftSum / ttftCount) : null,
88
+ latAvg: latCount ? Math.round(latSum / latCount) : null,
89
+ }))];
90
+ }));
91
+ }
92
+
93
+ export const taskId = z.union([z.string().min(1).max(128).regex(/^[\w.-]+$/), z.number().int().positive().safe()]).transform(String);
94
+ const identity = z.object({ nonce: taskId });
95
+ export const requests = {
96
+ snapshot: z.object({}).strict(),
97
+ task: z.object({ id: taskId }).strict(),
98
+ cancel: z.object({ id: taskId }).strict(),
99
+ test: identity.extend({ provider: z.string().min(1).max(512), model: z.string().min(1).max(512), prompt: z.string().max(16000).default('你好'), maxTokens: z.number().int().min(1).max(8192).default(256) }).strict(),
100
+ speed: identity.extend({ group: z.string().min(1).max(128) }).strict(),
101
+ };
102
+ const fault = (code, message) => Object.assign(new Error(message), { code });
103
+
104
+ /** Reserve before execution; duplicate ids never start a second billable request. */
105
+ export class TaskRegistry {
106
+ constructor({ persist = async () => {}, limit = 4 } = {}) {
107
+ this.persist = persist;
108
+ this.limit = limit;
109
+ this.tasks = new Map();
110
+ this.running = new Map();
111
+ this.queue = Promise.resolve();
112
+ this.closed = false;
113
+ }
114
+ restore(rows = []) {
115
+ for (const row of rows) {
116
+ if (!row || typeof row.id !== 'string') continue;
117
+ if (this.running.has(row.id)) continue;
118
+ this.tasks.set(row.id, row.status !== 'running' ? row : row.finishedAt
119
+ ? { ...row, status: row.ok === true ? 'ok' : 'error' }
120
+ : { ...row, status: 'error', code: 'ABORTED', error: '宿主已重启,任务未重放', finishedAt: Date.now() });
121
+ }
122
+ }
123
+ list() { return structuredClone([...this.tasks.values()]); }
124
+ get(id) {
125
+ const row = this.tasks.get(id);
126
+ if (!row) throw fault('TASK_NOT_FOUND', '任务不存在或已超过保留上限,请重新发起测试');
127
+ const { fingerprint, ...publicRow } = row;
128
+ return structuredClone(publicRow);
129
+ }
130
+ submit(kind, request, run) {
131
+ const reserve = async () => {
132
+ if (this.closed) throw fault('UNAVAILABLE', '插件正在停止');
133
+ const id = request.nonce;
134
+ const fingerprint = createHash('sha256').update(JSON.stringify([kind, request])).digest('hex');
135
+ const old = this.tasks.get(id);
136
+ if (old) {
137
+ if (old.fingerprint !== fingerprint) throw fault('TASK_CONFLICT', '该任务编号已用于另一项请求');
138
+ return this.get(id);
139
+ }
140
+ if (this.running.size >= this.limit) throw fault('BUSY', '同时执行的测试过多,请等待已有任务完成');
141
+ if (kind === 'speed' && [...this.tasks.values()].some(t => t.kind === 'speed' && t.group === request.group && t.status === 'running'))
142
+ throw fault('BUSY', '该轮询组正在测速');
143
+ const row = { id, kind, fingerprint, status: 'running', startedAt: Date.now(), ...(kind === 'test' ? { provider: request.provider, model: request.model } : { group: request.group }) };
144
+ this.tasks.set(id, row);
145
+ try { await this.persist(this.list()); }
146
+ catch (error) { this.tasks.delete(id); throw fault('STORAGE_ERROR', '任务未能保存,未发起模型请求:' + error.message); }
147
+ const controller = new AbortController();
148
+ const operation = { controller, done: null };
149
+ this.running.set(id, operation);
150
+ operation.done = Promise.resolve().then(() => { controller.signal.throwIfAborted(); return run(controller.signal); }).then(
151
+ result => ({ status: 'ok', ...result }),
152
+ error => ({ status: 'error', code: controller.signal.aborted ? 'ABORTED' : error.code || 'STREAM_ERROR', error: controller.signal.aborted ? '任务已取消' : String(error.message || error) }),
153
+ ).then(async result => {
154
+ if (controller.signal.aborted) result = { status: 'error', code: 'ABORTED', error: '任务已取消' };
155
+ this.tasks.set(id, { ...row, ...result, finishedAt: Date.now() });
156
+ const finished = [...this.tasks.values()].filter(t => t.status !== 'running').sort((a, b) => b.finishedAt - a.finishedAt);
157
+ for (const expired of finished.slice(100)) this.tasks.delete(expired.id);
158
+ try { await this.persist(this.list()); }
159
+ catch (error) { this.tasks.get(id).persistenceError = '结果仅保留在当前进程:' + error.message; }
160
+ }).finally(() => this.running.delete(id));
161
+ return this.get(id);
162
+ };
163
+ const pending = this.queue.then(reserve);
164
+ this.queue = pending.catch(() => {}); // Failed reservations do not block later independent tasks.
165
+ return pending;
166
+ }
167
+ cancel(id) {
168
+ const task = this.get(id);
169
+ this.running.get(id)?.controller.abort(fault('ABORTED', '任务已取消'));
170
+ return task;
171
+ }
172
+ async close() {
173
+ this.closed = true;
174
+ await this.queue;
175
+ for (const op of this.running.values()) op.controller.abort(fault('ABORTED', '插件已停止'));
176
+ await Promise.allSettled([...this.running.values()].map(op => op.done));
177
+ }
178
+ }
179
+
180
+ /** Connection already owns browser authentication and response framing. */
181
+ export function createRpcHandler({ snapshot, tasks, test, speed, ready = Promise.resolve() }) {
182
+ return async (endpoint, payload) => {
183
+ try {
184
+ if (!Object.hasOwn(requests, endpoint)) throw fault('NOT_FOUND', '未知操作');
185
+ const parsed = requests[endpoint].safeParse(payload);
186
+ if (!parsed.success) throw fault('BAD_REQUEST', parsed.error.issues.map(i => i.path.join('.') + ': ' + i.message).join('; '));
187
+ await (typeof ready === 'function' ? ready() : ready);
188
+ const req = parsed.data;
189
+ const value = endpoint === 'snapshot' ? snapshot()
190
+ : endpoint === 'task' ? tasks.get(req.id)
191
+ : endpoint === 'cancel' ? tasks.cancel(req.id)
192
+ : await tasks.submit(endpoint, req, signal => endpoint === 'test' ? test(req, signal) : speed(req, signal));
193
+ return { ok: true, value };
194
+ } catch (error) { return { ok: false, error: { code: error.code || 'UNAVAILABLE', message: String(error.message || error) } }; }
195
+ };
196
+ }