dsh-draw 0.1.0 → 0.1.2
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/CHANGELOG.md +20 -0
- package/README.es.md +5 -4
- package/README.hi.md +5 -4
- package/README.md +6 -4
- package/README.pt.md +5 -4
- package/README.zh.md +5 -4
- package/lib/index.js +137 -9
- package/lib/types/client/DrawSettingsTab.d.ts +1 -1
- package/lib/types/client/index.d.ts +5 -5
- package/lib/types/client/present.d.ts +1 -1
- package/lib/types/client/remote.d.ts +1 -1
- package/lib/types/drawer.d.ts +15 -5
- package/lib/types/drawer.d.ts.map +1 -1
- package/lib/types/drawer.js +7 -4
- package/lib/types/engine.d.ts +3 -3
- package/lib/types/event-gate.d.ts +46 -0
- package/lib/types/event-gate.d.ts.map +1 -0
- package/lib/types/event-gate.js +69 -0
- package/lib/types/index.d.ts +15 -14
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +7 -2
- package/lib/types/quota.d.ts +11 -5
- package/lib/types/quota.d.ts.map +1 -1
- package/lib/types/quota.js +16 -6
- package/lib/types/router.d.ts +3 -3
- package/lib/types/service.d.ts +4 -4
- package/lib/types/session-events.d.ts +32 -1
- package/lib/types/session-events.d.ts.map +1 -1
- package/lib/types/session-events.js +59 -3
- package/lib/types/tool.d.ts +2 -2
- package/lib/types/translate.d.ts +1 -1
- package/lib/types/version.d.ts +1 -1
- package/lib/types/version.js +1 -1
- package/lib/types/wire.d.ts +3 -3
- package/package.json +34 -29
- package/src/drawer.ts +14 -3
- package/src/event-gate.ts +84 -0
- package/src/index.ts +7 -2
- package/src/quota.ts +16 -6
- package/src/session-events.ts +63 -3
- package/src/version.ts +1 -1
package/src/drawer.ts
CHANGED
|
@@ -13,7 +13,8 @@ import type { ResolvedConfig } from './config.ts'
|
|
|
13
13
|
import type { EngineDeps, ProducedImage } from './engine.ts'
|
|
14
14
|
import { checkQuotaBytes, checkQuotaGenerations, type QuotaLimits, type QuotaState } from './quota.ts'
|
|
15
15
|
import { EngineRouter, type AttemptView } from './router.ts'
|
|
16
|
-
import {
|
|
16
|
+
import { commitDrawGenerated } from './session-events.ts'
|
|
17
|
+
import type { EventGate } from './event-gate.ts'
|
|
17
18
|
import { normalizeRequest } from './translate.ts'
|
|
18
19
|
|
|
19
20
|
/** One durable result image as the canonical value and wire carry it. */
|
|
@@ -102,6 +103,14 @@ export interface DrawerDeps {
|
|
|
102
103
|
sessions?: () => SessionStore | undefined
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
/** How the drawer commits the `draw/generated` accounting event. */
|
|
107
|
+
export interface DrawerEventSink {
|
|
108
|
+
/** The adaptive session-event gate, probed at mount. */
|
|
109
|
+
gate: EventGate
|
|
110
|
+
/** Log warning sink for append failures. */
|
|
111
|
+
warn: (message: string) => void
|
|
112
|
+
}
|
|
113
|
+
|
|
105
114
|
/**
|
|
106
115
|
* The shared generation path.
|
|
107
116
|
*/
|
|
@@ -110,11 +119,13 @@ export class Drawer {
|
|
|
110
119
|
* @param config - resolved plugin configuration.
|
|
111
120
|
* @param router - the engine router.
|
|
112
121
|
* @param deps - per-call dependencies (public: the remote service reads them for probes).
|
|
122
|
+
* @param events - the gated session-event sink (log when safe, in-memory ledger otherwise).
|
|
113
123
|
*/
|
|
114
124
|
constructor(
|
|
115
125
|
private readonly config: ResolvedConfig,
|
|
116
126
|
private readonly router: EngineRouter,
|
|
117
127
|
readonly deps: DrawerDeps,
|
|
128
|
+
private readonly events: DrawerEventSink,
|
|
118
129
|
) {}
|
|
119
130
|
|
|
120
131
|
/**
|
|
@@ -182,7 +193,7 @@ export class Drawer {
|
|
|
182
193
|
...(ref.name !== undefined ? { name: ref.name } : {}),
|
|
183
194
|
}))
|
|
184
195
|
const elapsedMs = Date.now() - startedAt
|
|
185
|
-
|
|
196
|
+
commitDrawGenerated(session, {
|
|
186
197
|
engine: routed.engine,
|
|
187
198
|
model: routed.model,
|
|
188
199
|
source: options.source,
|
|
@@ -193,7 +204,7 @@ export class Drawer {
|
|
|
193
204
|
bytes: totalBytes,
|
|
194
205
|
attachmentIds: images.map(image => image.attachmentId),
|
|
195
206
|
elapsedMs,
|
|
196
|
-
})
|
|
207
|
+
}, this.events.gate, this.events.warn)
|
|
197
208
|
const after = checkQuotaGenerations(session, limits)
|
|
198
209
|
return {
|
|
199
210
|
ok: true,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session-event adaptive gate. `draw/generated` is declared only in this
|
|
3
|
+
* package's types, so it sits outside every host's `KNOWN_SESSION_EVENT_TYPES`:
|
|
4
|
+
* hosts whose session reader validates a static whitelist (rc.6/rc.7) refuse to
|
|
5
|
+
* reopen a session containing the event unless it carries the `ignorable`
|
|
6
|
+
* envelope — and those hosts' `Session.append` cannot stamp it. The gate
|
|
7
|
+
* therefore appends only when the type is host-known or a mount-time probe
|
|
8
|
+
* proves envelope support; otherwise the accounting payload goes to the
|
|
9
|
+
* in-memory fallback ledger (see session-events.ts) so quota keeps working for
|
|
10
|
+
* the live session without poisoning the log.
|
|
11
|
+
*
|
|
12
|
+
* @module dsh-draw/event-gate
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Context } from '@deepseek-ai/cordis'
|
|
16
|
+
import SessionStore, { KNOWN_SESSION_EVENT_TYPES } from '@deepseek-ai/dsh-session'
|
|
17
|
+
|
|
18
|
+
/** One gate decision: whether to append, and whether to mark ignorable. */
|
|
19
|
+
export interface EventGateDecision {
|
|
20
|
+
/** Append the event to the session log. */
|
|
21
|
+
append: boolean
|
|
22
|
+
/** Stamp the `ignorable` envelope (only meaningful when append). */
|
|
23
|
+
ignorable: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Decision function over session event types. */
|
|
27
|
+
export type EventGate = (type: string) => EventGateDecision
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build the gate: host-known types append plainly; unknown types append with
|
|
31
|
+
* the ignorable envelope only when the host append honors it; everything else
|
|
32
|
+
* is skipped — an unknown, unmarked event would make the session refuse to
|
|
33
|
+
* load on restart under the rc.6/rc.7 static whitelist.
|
|
34
|
+
*
|
|
35
|
+
* @param knownTypes - the host's KNOWN_SESSION_EVENT_TYPES.
|
|
36
|
+
* @param ignorableAppend - whether the host append stamps the envelope.
|
|
37
|
+
* @returns the decision function.
|
|
38
|
+
*/
|
|
39
|
+
export function makeEventGate(knownTypes: ReadonlySet<string>, ignorableAppend = false): EventGate {
|
|
40
|
+
return (type) => {
|
|
41
|
+
if (knownTypes.has(type)) return { append: true, ignorable: false }
|
|
42
|
+
if (ignorableAppend) return { append: true, ignorable: true }
|
|
43
|
+
return { append: false, ignorable: false }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The append face the probe needs; rc.6 types declare no envelope parameter. */
|
|
48
|
+
type EnvelopeAppend = (type: string, data: unknown, opts: { ignorable: boolean }) => { ignorable?: boolean } | undefined
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Probe whether the host `Session.append` stamps the ignorable envelope. Runs
|
|
52
|
+
* on a fully detached SessionStore — never host persistence — appending one
|
|
53
|
+
* probe event with `{ ignorable: true }` and reading the marker back. rc.6's
|
|
54
|
+
* append silently drops the unknown options key (probe false); every failure
|
|
55
|
+
* is contained as "unsupported".
|
|
56
|
+
*
|
|
57
|
+
* @returns whether the envelope is honored.
|
|
58
|
+
*/
|
|
59
|
+
export function probeIgnorableAppend(): boolean {
|
|
60
|
+
try {
|
|
61
|
+
const store = new SessionStore(new Context())
|
|
62
|
+
const session = store.create()
|
|
63
|
+
const event = (session.append as unknown as EnvelopeAppend)('draw/generated', {
|
|
64
|
+
engine: 'probe',
|
|
65
|
+
model: 'probe',
|
|
66
|
+
source: 'tool',
|
|
67
|
+
prompt: 'probe',
|
|
68
|
+
size: 'auto',
|
|
69
|
+
quality: 'auto',
|
|
70
|
+
count: 0,
|
|
71
|
+
bytes: 0,
|
|
72
|
+
attachmentIds: [],
|
|
73
|
+
elapsedMs: 0,
|
|
74
|
+
}, { ignorable: true })
|
|
75
|
+
return event?.ignorable === true
|
|
76
|
+
} catch {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The gate over the host's known types plus the probed envelope support. */
|
|
82
|
+
export function makeHostEventGate(): EventGate {
|
|
83
|
+
return makeEventGate(KNOWN_SESSION_EVENT_TYPES, probeIgnorableAppend())
|
|
84
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type { SessionStore } from '@deepseek-ai/dsh-session'
|
|
|
22
22
|
import type {} from '@deepseek-ai/dsh-tools'
|
|
23
23
|
import { Config, resolveConfig } from './config.ts'
|
|
24
24
|
import { Drawer } from './drawer.ts'
|
|
25
|
+
import { makeHostEventGate } from './event-gate.ts'
|
|
25
26
|
import { defaultHttpTransport, type HttpTransport } from './http.ts'
|
|
26
27
|
import { EngineRouter } from './router.ts'
|
|
27
28
|
import { DrawService } from './service.ts'
|
|
@@ -42,7 +43,8 @@ export { callEngine, EngineCallError, type EngineDeps, type ProducedImage } from
|
|
|
42
43
|
export { translateRequest, normalizeRequest, type StandardImageRequest } from './translate.ts'
|
|
43
44
|
export { quotaState, checkQuotaGenerations, checkQuotaBytes, type QuotaLimits, type QuotaState } from './quota.ts'
|
|
44
45
|
export { sanitizeUrl, sanitizeText, sanitizeError, REDACTED } from './sanitize.ts'
|
|
45
|
-
export { appendDrawGenerated, drawGeneratedEvents, type DrawGeneratedEvent } from './session-events.ts'
|
|
46
|
+
export { appendDrawGenerated, commitDrawGenerated, drawGeneratedEvents, fallbackDrawGeneratedEvents, type DrawGeneratedEvent } from './session-events.ts'
|
|
47
|
+
export { makeEventGate, makeHostEventGate, probeIgnorableAppend, type EventGate, type EventGateDecision } from './event-gate.ts'
|
|
46
48
|
export { PLUGIN_VERSION } from './version.ts'
|
|
47
49
|
export { DRAW_INVOCATIONS, imageToWire, statusToView, probeToWire, type DrawStatusSnapshot } from './wire.ts'
|
|
48
50
|
|
|
@@ -72,6 +74,9 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
|
|
|
72
74
|
|
|
73
75
|
const credentials = () => ctx.get('credentials') as CredentialProvider | undefined
|
|
74
76
|
|
|
77
|
+
// The draw/generated event type is declared only by this package: append it
|
|
78
|
+
// only when the host knows the type or honors the ignorable envelope;
|
|
79
|
+
// otherwise the accounting payload rides the in-memory fallback ledger.
|
|
75
80
|
const drawer = new Drawer(resolved, router, {
|
|
76
81
|
engine: {
|
|
77
82
|
transport,
|
|
@@ -84,7 +89,7 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
|
|
|
84
89
|
},
|
|
85
90
|
attachments: () => ctx.get('attachments') as AttachmentStore | undefined,
|
|
86
91
|
sessions: () => ctx.get('sessions') as SessionStore | undefined,
|
|
87
|
-
})
|
|
92
|
+
}, { gate: makeHostEventGate(), warn: (message) => logger.warn(message) })
|
|
88
93
|
|
|
89
94
|
ctx.effect(() => ctx.tools.register(imageGenerateTool(drawer, resolved)), 'dsh-draw: image_generate tool')
|
|
90
95
|
|
package/src/quota.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-session quota accounting. The durable source of truth is the session
|
|
3
|
-
* log: every completed generation
|
|
4
|
-
* quota
|
|
5
|
-
*
|
|
3
|
+
* log: every completed generation commits one `draw/generated` event, and
|
|
4
|
+
* quota folds those events, so usage survives restart and fork and cannot
|
|
5
|
+
* drift from what the log records. On hosts that cannot carry the event
|
|
6
|
+
* safely (rc.6/rc.7 static whitelist without the ignorable envelope), commits
|
|
7
|
+
* land in the in-memory fallback ledger instead and quota folds both sources —
|
|
8
|
+
* live-session accounting stays exact; cross-restart durability resumes when
|
|
9
|
+
* the host gains a plugin event surface.
|
|
6
10
|
*
|
|
7
11
|
* @module dsh-draw/quota
|
|
8
12
|
*/
|
|
9
13
|
|
|
10
14
|
import type { Session } from '@deepseek-ai/dsh-session'
|
|
11
|
-
import { drawGeneratedEvents } from './session-events.ts'
|
|
15
|
+
import { drawGeneratedEvents, fallbackDrawGeneratedEvents } from './session-events.ts'
|
|
12
16
|
|
|
13
17
|
/** The two quota axes. */
|
|
14
18
|
export interface QuotaLimits {
|
|
@@ -48,9 +52,11 @@ export interface QuotaAllowance {
|
|
|
48
52
|
export type QuotaCheck = QuotaAllowance | QuotaDenial
|
|
49
53
|
|
|
50
54
|
/**
|
|
51
|
-
* Fold one session's `draw/generated` events into current usage
|
|
55
|
+
* Fold one session's `draw/generated` events into current usage: logged events
|
|
56
|
+
* first, then the in-memory fallback ledger (disjoint — a commit lands in
|
|
57
|
+
* exactly one of them).
|
|
52
58
|
*
|
|
53
|
-
* @param session - session whose log
|
|
59
|
+
* @param session - session whose log and ledger are folded.
|
|
54
60
|
* @returns generation and byte totals.
|
|
55
61
|
*/
|
|
56
62
|
export function quotaState(session: Session): QuotaState {
|
|
@@ -60,6 +66,10 @@ export function quotaState(session: Session): QuotaState {
|
|
|
60
66
|
generations += 1
|
|
61
67
|
bytes += event.data.bytes
|
|
62
68
|
}
|
|
69
|
+
for (const event of fallbackDrawGeneratedEvents(session)) {
|
|
70
|
+
generations += 1
|
|
71
|
+
bytes += event.bytes
|
|
72
|
+
}
|
|
63
73
|
return { generations, bytes }
|
|
64
74
|
}
|
|
65
75
|
|
package/src/session-events.ts
CHANGED
|
@@ -6,10 +6,18 @@
|
|
|
6
6
|
* output of the call ride the loop-owned `tool/call` and `tool/result`
|
|
7
7
|
* events; this event carries the accounting facts those events do not.
|
|
8
8
|
*
|
|
9
|
+
* The event type is declared only here, so plugin paths append it through
|
|
10
|
+
* {@link commitDrawGenerated}: the adaptive gate (event-gate.ts) appends only
|
|
11
|
+
* when the host knows the type or honors the `ignorable` envelope, and
|
|
12
|
+
* otherwise records the payload in the in-memory fallback ledger (live-session
|
|
13
|
+
* quota keeps working; the durable trail resumes once the host gains a plugin
|
|
14
|
+
* event surface).
|
|
15
|
+
*
|
|
9
16
|
* @module dsh-draw/session-events
|
|
10
17
|
*/
|
|
11
18
|
|
|
12
19
|
import type { Session } from '@deepseek-ai/dsh-session'
|
|
20
|
+
import type { EventGate } from './event-gate.ts'
|
|
13
21
|
|
|
14
22
|
declare module '@deepseek-ai/dsh-session/types' {
|
|
15
23
|
interface SessionEventMap {
|
|
@@ -46,18 +54,70 @@ export interface DrawGeneratedEvent {
|
|
|
46
54
|
}
|
|
47
55
|
|
|
48
56
|
/**
|
|
49
|
-
* Append one `draw/generated` event to a session.
|
|
57
|
+
* Append one `draw/generated` event to a session, ungated. Plugin paths use
|
|
58
|
+
* {@link commitDrawGenerated} instead; this raw form remains for tests and
|
|
59
|
+
* hosts that know the type.
|
|
50
60
|
*
|
|
51
61
|
* @param session - owning session.
|
|
52
62
|
* @param event - accounting payload.
|
|
53
63
|
* @returns the logged event.
|
|
54
64
|
*/
|
|
55
65
|
export function appendDrawGenerated(session: Session, event: DrawGeneratedEvent) {
|
|
56
|
-
// Two-argument append: the pinned 0.1.0-rc.6 peers have no append-envelope
|
|
57
|
-
// option; the two-argument form typechecks against rc.6 and newer builds.
|
|
58
66
|
return session.append('draw/generated', event)
|
|
59
67
|
}
|
|
60
68
|
|
|
69
|
+
/** The append face with the envelope option; rc.6 types declare none. */
|
|
70
|
+
type EnvelopeAppend = (type: string, data: unknown, opts: { ignorable: boolean }) => unknown
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* In-memory accounting ledger for hosts whose session log cannot carry
|
|
74
|
+
* `draw/generated` safely (rc.6/rc.7 static whitelist, no ignorable envelope).
|
|
75
|
+
* Keyed by Session identity: the entries live exactly as long as the session.
|
|
76
|
+
*/
|
|
77
|
+
const fallbackLedger = new WeakMap<Session, DrawGeneratedEvent[]>()
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Commit one completed generation: append the `draw/generated` event when the
|
|
81
|
+
* gate allows (with the ignorable envelope when required), otherwise record
|
|
82
|
+
* the payload in the in-memory fallback ledger. A failed append degrades to
|
|
83
|
+
* the same ledger and never breaks the draw.
|
|
84
|
+
*
|
|
85
|
+
* @param session - owning session.
|
|
86
|
+
* @param event - accounting payload.
|
|
87
|
+
* @param gate - the adaptive gate (event-gate.ts).
|
|
88
|
+
* @param warn - log warning sink for append failures.
|
|
89
|
+
*/
|
|
90
|
+
export function commitDrawGenerated(session: Session, event: DrawGeneratedEvent, gate: EventGate, warn: (message: string) => void): void {
|
|
91
|
+
const decision = gate('draw/generated')
|
|
92
|
+
if (decision.append) {
|
|
93
|
+
try {
|
|
94
|
+
if (decision.ignorable) {
|
|
95
|
+
;(session.append as unknown as EnvelopeAppend)('draw/generated', event, { ignorable: true })
|
|
96
|
+
} else {
|
|
97
|
+
session.append('draw/generated', event)
|
|
98
|
+
}
|
|
99
|
+
return
|
|
100
|
+
} catch (error) {
|
|
101
|
+
warn(`draw session event append failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const list = fallbackLedger.get(session) ?? []
|
|
105
|
+
list.push(event)
|
|
106
|
+
fallbackLedger.set(session, list)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The in-memory fallback records of a session, in commit order. Populated
|
|
111
|
+
* only while the host cannot carry `draw/generated` in the log; quota folds
|
|
112
|
+
* these on top of the logged events.
|
|
113
|
+
*
|
|
114
|
+
* @param session - session whose ledger is read.
|
|
115
|
+
* @returns the fallback payloads.
|
|
116
|
+
*/
|
|
117
|
+
export function fallbackDrawGeneratedEvents(session: Session): readonly DrawGeneratedEvent[] {
|
|
118
|
+
return fallbackLedger.get(session) ?? []
|
|
119
|
+
}
|
|
120
|
+
|
|
61
121
|
/**
|
|
62
122
|
* Collect the `draw/generated` events of a session in log order. Declaration
|
|
63
123
|
* merging narrows `event.type === 'draw/generated'` to the payload type.
|
package/src/version.ts
CHANGED