dsh-draw 0.1.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.
Files changed (104) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +201 -0
  3. package/README.es.md +194 -0
  4. package/README.hi.md +194 -0
  5. package/README.md +194 -0
  6. package/README.pt.md +194 -0
  7. package/README.zh.md +194 -0
  8. package/SECURITY.md +39 -0
  9. package/THIRD_PARTY_NOTICES.md +21 -0
  10. package/cordis.patch.yml +48 -0
  11. package/lib/client.js +4787 -0
  12. package/lib/client.js.map +1 -0
  13. package/lib/index.js +1429 -0
  14. package/lib/typert.host.js +26 -0
  15. package/lib/types/client/DrawResultCard.d.ts +58 -0
  16. package/lib/types/client/DrawResultCard.d.ts.map +1 -0
  17. package/lib/types/client/DrawResultCard.js +48 -0
  18. package/lib/types/client/DrawSettingsTab.d.ts +31 -0
  19. package/lib/types/client/DrawSettingsTab.d.ts.map +1 -0
  20. package/lib/types/client/DrawSettingsTab.js +129 -0
  21. package/lib/types/client/index.d.ts +35 -0
  22. package/lib/types/client/index.d.ts.map +1 -0
  23. package/lib/types/client/index.js +98 -0
  24. package/lib/types/client/locales.d.ts +14 -0
  25. package/lib/types/client/locales.d.ts.map +1 -0
  26. package/lib/types/client/locales.js +57 -0
  27. package/lib/types/client/present.d.ts +80 -0
  28. package/lib/types/client/present.d.ts.map +1 -0
  29. package/lib/types/client/present.js +86 -0
  30. package/lib/types/client/remote.d.ts +268 -0
  31. package/lib/types/client/remote.d.ts.map +1 -0
  32. package/lib/types/client/remote.js +15 -0
  33. package/lib/types/client/styles.d.ts +11 -0
  34. package/lib/types/client/styles.d.ts.map +1 -0
  35. package/lib/types/client/styles.js +43 -0
  36. package/lib/types/config.d.ts +160 -0
  37. package/lib/types/config.d.ts.map +1 -0
  38. package/lib/types/config.js +230 -0
  39. package/lib/types/drawer.d.ts +114 -0
  40. package/lib/types/drawer.d.ts.map +1 -0
  41. package/lib/types/drawer.js +138 -0
  42. package/lib/types/engine.d.ts +58 -0
  43. package/lib/types/engine.d.ts.map +1 -0
  44. package/lib/types/engine.js +135 -0
  45. package/lib/types/http.d.ts +89 -0
  46. package/lib/types/http.d.ts.map +1 -0
  47. package/lib/types/http.js +127 -0
  48. package/lib/types/index.d.ts +43 -0
  49. package/lib/types/index.d.ts.map +1 -0
  50. package/lib/types/index.js +78 -0
  51. package/lib/types/quota.d.ts +69 -0
  52. package/lib/types/quota.d.ts.map +1 -0
  53. package/lib/types/quota.js +56 -0
  54. package/lib/types/router.d.ts +141 -0
  55. package/lib/types/router.d.ts.map +1 -0
  56. package/lib/types/router.js +207 -0
  57. package/lib/types/sanitize.d.ts +40 -0
  58. package/lib/types/sanitize.d.ts.map +1 -0
  59. package/lib/types/sanitize.js +103 -0
  60. package/lib/types/service.d.ts +59 -0
  61. package/lib/types/service.d.ts.map +1 -0
  62. package/lib/types/service.js +131 -0
  63. package/lib/types/session-events.d.ts +66 -0
  64. package/lib/types/session-events.d.ts.map +1 -0
  65. package/lib/types/session-events.js +32 -0
  66. package/lib/types/tool.d.ts +30 -0
  67. package/lib/types/tool.d.ts.map +1 -0
  68. package/lib/types/tool.js +131 -0
  69. package/lib/types/translate.d.ts +64 -0
  70. package/lib/types/translate.d.ts.map +1 -0
  71. package/lib/types/translate.js +56 -0
  72. package/lib/types/typert.host.d.ts +250 -0
  73. package/lib/types/typert.host.d.ts.map +1 -0
  74. package/lib/types/typert.host.js +23 -0
  75. package/lib/types/version.d.ts +10 -0
  76. package/lib/types/version.d.ts.map +1 -0
  77. package/lib/types/version.js +9 -0
  78. package/lib/types/wire.d.ts +699 -0
  79. package/lib/types/wire.d.ts.map +1 -0
  80. package/lib/types/wire.js +273 -0
  81. package/lib/wire-Cc4JZ3jR.js +4370 -0
  82. package/package.json +179 -0
  83. package/src/client/DrawResultCard.tsx +100 -0
  84. package/src/client/DrawSettingsTab.tsx +159 -0
  85. package/src/client/index.ts +123 -0
  86. package/src/client/locales.ts +84 -0
  87. package/src/client/present.ts +137 -0
  88. package/src/client/remote.ts +44 -0
  89. package/src/client/styles.ts +44 -0
  90. package/src/config.ts +358 -0
  91. package/src/drawer.ts +234 -0
  92. package/src/engine.ts +182 -0
  93. package/src/http.ts +161 -0
  94. package/src/index.ts +93 -0
  95. package/src/quota.ts +98 -0
  96. package/src/router.ts +309 -0
  97. package/src/sanitize.ts +113 -0
  98. package/src/service.ts +169 -0
  99. package/src/session-events.ts +70 -0
  100. package/src/tool.ts +145 -0
  101. package/src/translate.ts +101 -0
  102. package/src/typert.host.ts +25 -0
  103. package/src/version.ts +10 -0
  104. package/src/wire.ts +417 -0
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Plugin configuration for `dsh-draw` and its explicit resolve step.
3
+ * `resolveConfig` re-judges every default and bound so programmatic
4
+ * construction that bypasses Schemastery normalization still fails loud
5
+ * instead of running with hidden defaults (the explicit-resolve contract).
6
+ *
7
+ * @module dsh-draw/config
8
+ */
9
+ import z from '@deepseek-ai/schemastery';
10
+ /** Default per-generation HTTP timeout in milliseconds. */
11
+ export const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
12
+ /** Ceiling for one generation call: engines can take minutes per image. */
13
+ export const MAX_REQUEST_TIMEOUT_MS = 600_000;
14
+ /** Default cap on images a single call may produce. */
15
+ export const DEFAULT_MAX_IMAGES_PER_CALL = 4;
16
+ /** Ceiling on images a single call may produce. */
17
+ export const MAX_IMAGES_PER_CALL = 10;
18
+ /** Default prompt length cap (characters). */
19
+ export const DEFAULT_MAX_PROMPT_LENGTH = 4_000;
20
+ /** Ceiling on the prompt length cap. */
21
+ export const MAX_PROMPT_LENGTH = 32_000;
22
+ /** Default per-session generation-call cap. */
23
+ export const DEFAULT_MAX_GENERATIONS_PER_SESSION = 200;
24
+ /** Ceiling on the per-session generation-call cap. */
25
+ export const MAX_GENERATIONS_PER_SESSION = 100_000;
26
+ /** Default per-session image-byte cap (200 MiB). */
27
+ export const DEFAULT_MAX_BYTES_PER_SESSION = 200 * 1024 * 1024;
28
+ /** Ceiling on the per-session image-byte cap (4 GiB). */
29
+ export const MAX_BYTES_PER_SESSION = 4 * 1024 * 1024 * 1024;
30
+ /** Default consecutive failures before an engine enters cooldown. */
31
+ export const DEFAULT_FAILURE_THRESHOLD = 2;
32
+ /** Ceiling on the consecutive-failure threshold. */
33
+ export const MAX_FAILURE_THRESHOLD = 10;
34
+ /** Default engine cooldown after the failure threshold trips (ms). */
35
+ export const DEFAULT_COOLDOWN_MS = 60_000;
36
+ /** Ceiling on the engine cooldown. */
37
+ export const MAX_COOLDOWN_MS = 3_600_000;
38
+ /** The two shipped engine presets: OpenAI Images and Zhipu CogView. */
39
+ export const DEFAULT_ENGINES = Object.freeze([
40
+ Object.freeze({
41
+ id: 'openai',
42
+ baseUrl: 'https://api.openai.com/v1',
43
+ model: 'gpt-image-1',
44
+ apiKeyRef: 'OPENAI_API_KEY',
45
+ enabled: true,
46
+ sizeMap: Object.freeze({ square: '1024x1024', landscape: '1536x1024', portrait: '1024x1536', auto: 'auto' }),
47
+ qualitySupported: true,
48
+ styleSupported: true,
49
+ responseFormat: 'b64_json',
50
+ imageMediaType: 'image/png',
51
+ }),
52
+ Object.freeze({
53
+ id: 'cogview',
54
+ baseUrl: 'https://open.bigmodel.cn/api/paas/v4',
55
+ model: 'cogview-3-flash',
56
+ apiKeyRef: 'ZHIPU_API_KEY',
57
+ enabled: true,
58
+ sizeMap: Object.freeze({ square: '1024x1024', landscape: '1344x768', portrait: '768x1344', auto: '1024x1024' }),
59
+ qualitySupported: false,
60
+ styleSupported: false,
61
+ responseFormat: 'url',
62
+ imageMediaType: 'image/png',
63
+ }),
64
+ ]);
65
+ /** Default size map for user-configured engines (the OpenAI vocabulary). */
66
+ export const DEFAULT_SIZE_MAP = Object.freeze({
67
+ square: '1024x1024',
68
+ landscape: '1536x1024',
69
+ portrait: '1024x1536',
70
+ auto: 'auto',
71
+ });
72
+ /** Schemastery schema for loader-validated configuration. */
73
+ export const Config = z.object({
74
+ engines: z.array(z.object({
75
+ id: z.string().min(1).max(64),
76
+ baseUrl: z.string().min(1).max(2048),
77
+ model: z.string().min(1).max(128),
78
+ apiKeyRef: z.string().min(1).max(128),
79
+ enabled: z.boolean(),
80
+ sizeMap: z.object({
81
+ square: z.string().min(1).max(64),
82
+ landscape: z.string().min(1).max(64),
83
+ portrait: z.string().min(1).max(64),
84
+ auto: z.string().min(1).max(64),
85
+ }),
86
+ qualitySupported: z.boolean(),
87
+ styleSupported: z.boolean(),
88
+ responseFormat: z.union(['b64_json', 'url']),
89
+ imageMediaType: z.union(['image/png', 'image/jpeg', 'image/webp', 'image/gif']),
90
+ })).default([...DEFAULT_ENGINES]),
91
+ defaultEngine: z.string().min(1).max(64).default('openai'),
92
+ requestTimeoutMs: z.number().min(1_000).max(MAX_REQUEST_TIMEOUT_MS).default(DEFAULT_REQUEST_TIMEOUT_MS),
93
+ maxImagesPerCall: z.number().min(1).max(MAX_IMAGES_PER_CALL).default(DEFAULT_MAX_IMAGES_PER_CALL),
94
+ maxPromptLength: z.number().min(1).max(MAX_PROMPT_LENGTH).default(DEFAULT_MAX_PROMPT_LENGTH),
95
+ maxGenerationsPerSession: z.number().min(1).max(MAX_GENERATIONS_PER_SESSION).default(DEFAULT_MAX_GENERATIONS_PER_SESSION),
96
+ maxBytesPerSession: z.number().min(1024 * 1024).max(MAX_BYTES_PER_SESSION).default(DEFAULT_MAX_BYTES_PER_SESSION),
97
+ failureThreshold: z.number().min(1).max(MAX_FAILURE_THRESHOLD).default(DEFAULT_FAILURE_THRESHOLD),
98
+ cooldownMs: z.number().min(1_000).max(MAX_COOLDOWN_MS).default(DEFAULT_COOLDOWN_MS),
99
+ });
100
+ const ENGINE_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/u;
101
+ /** Credential references are environment-variable names (POSIX identifiers). */
102
+ const API_KEY_REF_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u;
103
+ /**
104
+ * Resolve raw config to the runtime policy, re-validating defaults and bounds
105
+ * plus the cross-field facts a per-field schema cannot see (duplicate engine
106
+ * ids, credential-bearing base URLs, an unknown default engine).
107
+ *
108
+ * @param config - raw loader config; `undefined` for a bare row.
109
+ * @returns the frozen resolved config.
110
+ * @throws on any invalid value (fail-loud at load).
111
+ */
112
+ export function resolveConfig(config) {
113
+ const rawEngines = config?.engines ?? DEFAULT_ENGINES;
114
+ const engines = [];
115
+ const seen = new Set();
116
+ for (const engine of rawEngines) {
117
+ const id = typeof engine?.id === 'string' ? engine.id : '';
118
+ if (!ENGINE_ID_PATTERN.test(id)) {
119
+ throw new Error(`dsh-draw: engine id ${JSON.stringify(id)} must match ${ENGINE_ID_PATTERN}`);
120
+ }
121
+ if (seen.has(id))
122
+ throw new Error(`dsh-draw: duplicate engine id ${JSON.stringify(id)}`);
123
+ seen.add(id);
124
+ const baseUrl = engine.baseUrl ?? '';
125
+ let parsed;
126
+ try {
127
+ parsed = new URL(baseUrl);
128
+ }
129
+ catch {
130
+ throw new Error(`dsh-draw: engine "${id}" baseUrl ${JSON.stringify(baseUrl)} is not a valid URL`);
131
+ }
132
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
133
+ throw new Error(`dsh-draw: engine "${id}" baseUrl must use http or https`);
134
+ }
135
+ if (parsed.username !== '' || parsed.password !== '') {
136
+ throw new Error(`dsh-draw: engine "${id}" baseUrl must not embed credentials — use the apiKeyRef credential reference instead`);
137
+ }
138
+ const model = typeof engine.model === 'string' && engine.model.length > 0 && engine.model.length <= 128 ? engine.model : '';
139
+ if (model === '')
140
+ throw new Error(`dsh-draw: engine "${id}" model must be a non-empty string of at most 128 characters`);
141
+ const apiKeyRef = typeof engine.apiKeyRef === 'string' && API_KEY_REF_PATTERN.test(engine.apiKeyRef) ? engine.apiKeyRef : '';
142
+ if (apiKeyRef === '')
143
+ throw new Error(`dsh-draw: engine "${id}" apiKeyRef ${JSON.stringify(engine.apiKeyRef)} must be an environment-variable name`);
144
+ const sizeMap = engine.sizeMap === undefined ? DEFAULT_SIZE_MAP : engine.sizeMap;
145
+ for (const key of ['square', 'landscape', 'portrait', 'auto']) {
146
+ const value = sizeMap[key];
147
+ if (typeof value !== 'string' || value.length === 0 || value.length > 64) {
148
+ throw new Error(`dsh-draw: engine "${id}" sizeMap.${key} must be a non-empty string of at most 64 characters`);
149
+ }
150
+ }
151
+ const enabled = engine.enabled ?? true;
152
+ if (typeof enabled !== 'boolean')
153
+ throw new TypeError(`dsh-draw: engine "${id}" enabled must be a boolean`);
154
+ const qualitySupported = engine.qualitySupported ?? false;
155
+ if (typeof qualitySupported !== 'boolean')
156
+ throw new TypeError(`dsh-draw: engine "${id}" qualitySupported must be a boolean`);
157
+ const styleSupported = engine.styleSupported ?? false;
158
+ if (typeof styleSupported !== 'boolean')
159
+ throw new TypeError(`dsh-draw: engine "${id}" styleSupported must be a boolean`);
160
+ const responseFormat = engine.responseFormat ?? 'b64_json';
161
+ if (responseFormat !== 'b64_json' && responseFormat !== 'url') {
162
+ throw new Error(`dsh-draw: engine "${id}" responseFormat must be "b64_json" or "url"`);
163
+ }
164
+ const imageMediaType = engine.imageMediaType ?? 'image/png';
165
+ if (imageMediaType !== 'image/png' && imageMediaType !== 'image/jpeg' && imageMediaType !== 'image/webp' && imageMediaType !== 'image/gif') {
166
+ throw new Error(`dsh-draw: engine "${id}" imageMediaType ${JSON.stringify(imageMediaType)} is not a supported raster type`);
167
+ }
168
+ engines.push({
169
+ id,
170
+ baseUrl: baseUrl.replace(/\/+$/u, ''),
171
+ model,
172
+ apiKeyRef,
173
+ enabled,
174
+ sizeMap: Object.freeze({ ...sizeMap }),
175
+ qualitySupported,
176
+ styleSupported,
177
+ responseFormat,
178
+ imageMediaType,
179
+ });
180
+ }
181
+ if (engines.length === 0)
182
+ throw new Error('dsh-draw: at least one engine must be configured');
183
+ const defaultEngine = config?.defaultEngine ?? engines[0].id;
184
+ if (!seen.has(defaultEngine)) {
185
+ throw new Error(`dsh-draw: defaultEngine ${JSON.stringify(defaultEngine)} does not name a configured engine`);
186
+ }
187
+ const requestTimeoutMs = config?.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
188
+ if (!Number.isFinite(requestTimeoutMs) || requestTimeoutMs < 1_000 || requestTimeoutMs > MAX_REQUEST_TIMEOUT_MS) {
189
+ throw new Error(`dsh-draw: config.requestTimeoutMs must be a finite number between 1000 and ${MAX_REQUEST_TIMEOUT_MS}`);
190
+ }
191
+ const maxImagesPerCall = config?.maxImagesPerCall ?? DEFAULT_MAX_IMAGES_PER_CALL;
192
+ if (!Number.isInteger(maxImagesPerCall) || maxImagesPerCall < 1 || maxImagesPerCall > MAX_IMAGES_PER_CALL) {
193
+ throw new Error(`dsh-draw: config.maxImagesPerCall must be an integer between 1 and ${MAX_IMAGES_PER_CALL}`);
194
+ }
195
+ const maxPromptLength = config?.maxPromptLength ?? DEFAULT_MAX_PROMPT_LENGTH;
196
+ if (!Number.isInteger(maxPromptLength) || maxPromptLength < 1 || maxPromptLength > MAX_PROMPT_LENGTH) {
197
+ throw new Error(`dsh-draw: config.maxPromptLength must be an integer between 1 and ${MAX_PROMPT_LENGTH}`);
198
+ }
199
+ const maxGenerationsPerSession = config?.maxGenerationsPerSession ?? DEFAULT_MAX_GENERATIONS_PER_SESSION;
200
+ if (!Number.isInteger(maxGenerationsPerSession) || maxGenerationsPerSession < 1 || maxGenerationsPerSession > MAX_GENERATIONS_PER_SESSION) {
201
+ throw new Error(`dsh-draw: config.maxGenerationsPerSession must be an integer between 1 and ${MAX_GENERATIONS_PER_SESSION}`);
202
+ }
203
+ const maxBytesPerSession = config?.maxBytesPerSession ?? DEFAULT_MAX_BYTES_PER_SESSION;
204
+ if (!Number.isInteger(maxBytesPerSession) || maxBytesPerSession < 1024 * 1024 || maxBytesPerSession > MAX_BYTES_PER_SESSION) {
205
+ throw new Error(`dsh-draw: config.maxBytesPerSession must be an integer between 1048576 and ${MAX_BYTES_PER_SESSION}`);
206
+ }
207
+ const failureThreshold = config?.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD;
208
+ if (!Number.isInteger(failureThreshold) || failureThreshold < 1 || failureThreshold > MAX_FAILURE_THRESHOLD) {
209
+ throw new Error(`dsh-draw: config.failureThreshold must be an integer between 1 and ${MAX_FAILURE_THRESHOLD}`);
210
+ }
211
+ const cooldownMs = config?.cooldownMs ?? DEFAULT_COOLDOWN_MS;
212
+ if (!Number.isInteger(cooldownMs) || cooldownMs < 1_000 || cooldownMs > MAX_COOLDOWN_MS) {
213
+ throw new Error(`dsh-draw: config.cooldownMs must be an integer between 1000 and ${MAX_COOLDOWN_MS}`);
214
+ }
215
+ return Object.freeze({
216
+ engines: Object.freeze(engines),
217
+ defaultEngine,
218
+ requestTimeoutMs,
219
+ maxImagesPerCall,
220
+ maxPromptLength,
221
+ maxGenerationsPerSession,
222
+ maxBytesPerSession,
223
+ failureThreshold,
224
+ cooldownMs,
225
+ });
226
+ }
227
+ /** Engine id by which an engine is addressed, or `undefined` for an unknown id. */
228
+ export function engineById(config, id) {
229
+ return config.engines.find(engine => engine.id === id);
230
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The generation drawer: the shared orchestration behind the `image_generate`
3
+ * tool body and the settings-panel/card regenerate action. One path owns
4
+ * validation, quota, routing, durable attachment storage, and the session
5
+ * audit event, so the two entry points can never drift.
6
+ *
7
+ * @module dsh-draw/drawer
8
+ */
9
+ import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
10
+ import type { Session, SessionStore } from '@deepseek-ai/dsh-session';
11
+ import type { ResolvedConfig } from './config.ts';
12
+ import type { EngineDeps } from './engine.ts';
13
+ import { type QuotaLimits, type QuotaState } from './quota.ts';
14
+ import { EngineRouter, type AttemptView } from './router.ts';
15
+ /** One durable result image as the canonical value and wire carry it. */
16
+ export interface DrawImage {
17
+ /** Opaque attachment id. */
18
+ attachmentId: string;
19
+ /** Verified media type. */
20
+ mediaType: string;
21
+ /** Exact byte length. */
22
+ bytes: number;
23
+ /** Intrinsic width in pixels. */
24
+ width: number;
25
+ /** Intrinsic height in pixels. */
26
+ height: number;
27
+ /** Display name. */
28
+ name?: string;
29
+ }
30
+ /** A successful draw. */
31
+ export interface DrawSuccess {
32
+ /** Discriminant. */
33
+ ok: true;
34
+ /** Engine id that produced the images. */
35
+ engine: string;
36
+ /** Engine model name. */
37
+ model: string;
38
+ /** Standard size vocabulary value of the request. */
39
+ size: 'square' | 'landscape' | 'portrait' | 'auto';
40
+ /** Durable result images. */
41
+ images: readonly DrawImage[];
42
+ /** Usage after this generation committed. */
43
+ quota: QuotaState;
44
+ /** Quota limits in force. */
45
+ limits: QuotaLimits;
46
+ /** Whether an earlier engine in the chain failed first. */
47
+ fallbackUsed: boolean;
48
+ /** Engine round-trip latency in milliseconds. */
49
+ elapsedMs: number;
50
+ /** Every router attempt in chain order. */
51
+ attempts: readonly AttemptView[];
52
+ }
53
+ /** Why a draw failed before or during routing. */
54
+ export type DrawFailureReason = 'invalid-prompt' | 'quota-generations' | 'quota-bytes' | 'no-session' | 'attachments-unavailable' | 'all-engines-failed';
55
+ /** A failed draw with a machine-routable reason and display-safe message. */
56
+ export interface DrawFailure {
57
+ /** Discriminant. */
58
+ ok: false;
59
+ /** Which stage blocked the draw. */
60
+ reason: DrawFailureReason;
61
+ /** Display-safe explanation. */
62
+ message: string;
63
+ /** Quota usage at decision time for quota failures. */
64
+ quota?: QuotaState;
65
+ /** Router attempts for engine failures. */
66
+ attempts?: readonly AttemptView[];
67
+ }
68
+ /** Draw outcome. */
69
+ export type DrawOutcome = DrawSuccess | DrawFailure;
70
+ /** Options for one generation. */
71
+ export interface DrawOptions {
72
+ /** Caller cancellation. */
73
+ signal?: AbortSignal;
74
+ /** Owning session (required: quota and the audit event live in the log). */
75
+ session: Session | undefined;
76
+ /** Who requested the generation (recorded in the audit event). */
77
+ source: 'tool' | 'regenerate';
78
+ }
79
+ /** Drawer dependencies, resolved per call so optional services stay hot-swappable. */
80
+ export interface DrawerDeps {
81
+ /** Engine transport and credential resolution. */
82
+ engine: EngineDeps;
83
+ /** Per-call durable attachment store accessor (absent fails the draw closed). */
84
+ attachments?: () => AttachmentStore | undefined;
85
+ /** Per-call session store accessor for the regenerate path. */
86
+ sessions?: () => SessionStore | undefined;
87
+ }
88
+ /**
89
+ * The shared generation path.
90
+ */
91
+ export declare class Drawer {
92
+ private readonly config;
93
+ private readonly router;
94
+ readonly deps: DrawerDeps;
95
+ /**
96
+ * @param config - resolved plugin configuration.
97
+ * @param router - the engine router.
98
+ * @param deps - per-call dependencies (public: the remote service reads them for probes).
99
+ */
100
+ constructor(config: ResolvedConfig, router: EngineRouter, deps: DrawerDeps);
101
+ /**
102
+ * Run one generation end to end: normalize and validate, check quota,
103
+ * route through the engine chain, commit images to the attachment store,
104
+ * and append the audit event.
105
+ *
106
+ * @param args - unvalidated standard request (tool args or card regenerate args).
107
+ * @param options - cancellation, session, and source.
108
+ * @returns the outcome.
109
+ */
110
+ generate(args: unknown, options: DrawOptions): Promise<DrawOutcome>;
111
+ /** Save every produced image to the attachment store; an empty image fails the batch. */
112
+ private saveImages;
113
+ }
114
+ //# sourceMappingURL=drawer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drawer.d.ts","sourceRoot":"","sources":["../../src/drawer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * The generation drawer: the shared orchestration behind the `image_generate`
3
+ * tool body and the settings-panel/card regenerate action. One path owns
4
+ * validation, quota, routing, durable attachment storage, and the session
5
+ * audit event, so the two entry points can never drift.
6
+ *
7
+ * @module dsh-draw/drawer
8
+ */
9
+ import { checkQuotaBytes, checkQuotaGenerations } from './quota.js';
10
+ import { appendDrawGenerated } from './session-events.js';
11
+ import { normalizeRequest } from './translate.js';
12
+ /**
13
+ * The shared generation path.
14
+ */
15
+ export class Drawer {
16
+ config;
17
+ router;
18
+ deps;
19
+ /**
20
+ * @param config - resolved plugin configuration.
21
+ * @param router - the engine router.
22
+ * @param deps - per-call dependencies (public: the remote service reads them for probes).
23
+ */
24
+ constructor(config, router, deps) {
25
+ this.config = config;
26
+ this.router = router;
27
+ this.deps = deps;
28
+ }
29
+ /**
30
+ * Run one generation end to end: normalize and validate, check quota,
31
+ * route through the engine chain, commit images to the attachment store,
32
+ * and append the audit event.
33
+ *
34
+ * @param args - unvalidated standard request (tool args or card regenerate args).
35
+ * @param options - cancellation, session, and source.
36
+ * @returns the outcome.
37
+ */
38
+ async generate(args, options) {
39
+ const request = normalizeRequest(args, this.config.maxImagesPerCall);
40
+ if (request.prompt.trim().length === 0) {
41
+ return { ok: false, reason: 'invalid-prompt', message: 'image_generate: prompt must be a non-empty string' };
42
+ }
43
+ if (request.prompt.length > this.config.maxPromptLength) {
44
+ return { ok: false, reason: 'invalid-prompt', message: `image_generate: prompt exceeds the configured ${this.config.maxPromptLength}-character cap` };
45
+ }
46
+ const session = options.session;
47
+ if (session === undefined) {
48
+ return { ok: false, reason: 'no-session', message: 'image_generate: no session owns this call — quota accounting and the audit event need a session' };
49
+ }
50
+ const limits = {
51
+ maxGenerations: this.config.maxGenerationsPerSession,
52
+ maxBytes: this.config.maxBytesPerSession,
53
+ };
54
+ const generationCheck = checkQuotaGenerations(session, limits);
55
+ if (!generationCheck.allowed) {
56
+ return { ok: false, reason: 'quota-generations', message: `image_generate: session generation quota exhausted (${generationCheck.state.generations}/${limits.maxGenerations} calls)`, quota: generationCheck.state };
57
+ }
58
+ const startedAt = Date.now();
59
+ const routed = await this.router.generate(request, this.deps.engine, options.signal);
60
+ if (!routed.ok) {
61
+ return {
62
+ ok: false,
63
+ reason: 'all-engines-failed',
64
+ message: `image_generate: no configured engine produced images${routed.attempts.length === 0 ? '' : ` (${routed.attempts.map(attempt => attempt.engine).join(', ')})`}`,
65
+ attempts: routed.attempts,
66
+ quota: generationCheck.state,
67
+ };
68
+ }
69
+ const attachments = this.deps.attachments?.();
70
+ if (attachments === undefined) {
71
+ return { ok: false, reason: 'attachments-unavailable', message: 'image_generate: the attachment store is not composed — images cannot be saved durably', quota: generationCheck.state, attempts: routed.attempts };
72
+ }
73
+ const totalBytes = routed.images.reduce((sum, image) => sum + image.data.byteLength, 0);
74
+ const byteCheck = checkQuotaBytes(session, limits, totalBytes);
75
+ if (!byteCheck.allowed) {
76
+ return { ok: false, reason: 'quota-bytes', message: `image_generate: session image-byte quota exhausted (${byteCheck.state.bytes}/${limits.maxBytes} bytes)`, quota: byteCheck.state, attempts: routed.attempts };
77
+ }
78
+ let refs;
79
+ try {
80
+ refs = await this.saveImages(attachments, routed.engine, routed.images, options.signal);
81
+ }
82
+ catch (error) {
83
+ const message = error instanceof Error ? error.message : String(error);
84
+ return { ok: false, reason: 'attachments-unavailable', message: `image_generate: saving images failed: ${message}`, quota: generationCheck.state, attempts: routed.attempts };
85
+ }
86
+ const images = refs.map(ref => ({
87
+ attachmentId: String(ref.attachmentId),
88
+ mediaType: ref.mediaType,
89
+ bytes: ref.bytes,
90
+ width: ref.width,
91
+ height: ref.height,
92
+ ...(ref.name !== undefined ? { name: ref.name } : {}),
93
+ }));
94
+ const elapsedMs = Date.now() - startedAt;
95
+ appendDrawGenerated(session, {
96
+ engine: routed.engine,
97
+ model: routed.model,
98
+ source: options.source,
99
+ prompt: request.prompt,
100
+ size: request.size ?? 'square',
101
+ quality: request.quality ?? 'auto',
102
+ count: images.length,
103
+ bytes: totalBytes,
104
+ attachmentIds: images.map(image => image.attachmentId),
105
+ elapsedMs,
106
+ });
107
+ const after = checkQuotaGenerations(session, limits);
108
+ return {
109
+ ok: true,
110
+ engine: routed.engine,
111
+ model: routed.model,
112
+ size: request.size ?? 'square',
113
+ images,
114
+ quota: after.state,
115
+ limits,
116
+ fallbackUsed: routed.fallbackUsed,
117
+ elapsedMs,
118
+ attempts: routed.attempts,
119
+ };
120
+ }
121
+ /** Save every produced image to the attachment store; an empty image fails the batch. */
122
+ async saveImages(attachments, engineId, produced, signal) {
123
+ const refs = [];
124
+ for (let index = 0; index < produced.length; index += 1) {
125
+ signal?.throwIfAborted();
126
+ const image = produced[index];
127
+ if (image.data.byteLength === 0) {
128
+ throw new Error(`engine "${engineId}" produced an empty image`);
129
+ }
130
+ refs.push(await attachments.saveImage({
131
+ data: image.data,
132
+ mediaType: image.mediaType,
133
+ name: `${engineId}-${index + 1}.${image.mediaType === 'image/jpeg' ? 'jpg' : image.mediaType.slice('image/'.length)}`,
134
+ }));
135
+ }
136
+ return refs;
137
+ }
138
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The OpenAI-compatible images adapter: one implementation covers OpenAI
3
+ * Images, Zhipu CogView, and any config-driven compatible endpoint. The
4
+ * request is a `POST {baseUrl}/images/generations` JSON body; the response is
5
+ * `{ data: [{ b64_json } | { url }] }`. Engines declare their response format
6
+ * and media type in config, so no provider-specific code branches exist.
7
+ *
8
+ * @module dsh-draw/engine
9
+ */
10
+ import type { ImageMediaType } from '@deepseek-ai/dsh-attachment';
11
+ import type { ResolvedEngineConfig } from './config.ts';
12
+ import { type HttpTransport } from './http.ts';
13
+ import type { TranslatedImageRequest } from './translate.ts';
14
+ /** Failure phases of one engine call, each mapping to a router decision. */
15
+ export type EngineFailurePhase = 'credential' | 'request' | 'parse';
16
+ /**
17
+ * A single engine call failure. `message` is display-safe (never carries the
18
+ * API key); `status` carries the HTTP status when a response existed.
19
+ */
20
+ export declare class EngineCallError extends Error {
21
+ /** Which stage failed. */
22
+ readonly phase: EngineFailurePhase;
23
+ /** Stable machine code: `unconfigured`, `auth`, `http`, `parse`. */
24
+ readonly code: string;
25
+ /** HTTP status when a response existed. */
26
+ readonly status?: number;
27
+ /** @param phase - failing stage. @param code - stable code. @param message - display-safe message. @param options - optional status and cause. */
28
+ constructor(phase: EngineFailurePhase, code: string, message: string, options?: {
29
+ status?: number;
30
+ cause?: unknown;
31
+ });
32
+ }
33
+ /** One produced image: bytes plus the engine-declared media type. */
34
+ export interface ProducedImage {
35
+ /** Encoded image bytes. */
36
+ data: Uint8Array;
37
+ /** Engine-declared media type (validated by the attachment store). */
38
+ mediaType: ImageMediaType;
39
+ }
40
+ /** Dependencies the engine call resolves per operation. */
41
+ export interface EngineDeps {
42
+ /** HTTP transport for the images request and any URL download. */
43
+ transport: HttpTransport;
44
+ /** Resolve the engine's credential reference to a secret value (per call; never cached). */
45
+ resolveCredential: (reference: string) => Promise<string | undefined>;
46
+ }
47
+ /**
48
+ * Call one engine for the given translated request.
49
+ *
50
+ * @param engine - resolved engine configuration.
51
+ * @param request - translated request body fields.
52
+ * @param deps - transport and credential resolution.
53
+ * @param signal - caller cancellation.
54
+ * @returns the produced images.
55
+ * @throws {@link EngineCallError} with a phase the router can act on.
56
+ */
57
+ export declare function callEngine(engine: ResolvedEngineConfig, request: TranslatedImageRequest, deps: EngineDeps, signal?: AbortSignal): Promise<ProducedImage[]>;
58
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AACjE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AACvD,OAAO,EAA6B,KAAK,aAAa,EAAE,MAAM,WAAW,CAAA;AAEzE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AAE5D,4EAA4E;AAC5E,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG,SAAS,GAAG,OAAO,CAAA;AAEnE;;;GAGG;AACH,qBAAa,eAAgB,SAAQ,KAAK;IACxC,0BAA0B;IAC1B,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAA;IAClC,oEAAoE;IACpE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,2CAA2C;IAC3C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,kJAAkJ;IAClJ,YAAY,KAAK,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAMnH;CACF;AAED,qEAAqE;AACrE,MAAM,WAAW,aAAa;IAC5B,2BAA2B;IAC3B,IAAI,EAAE,UAAU,CAAA;IAChB,sEAAsE;IACtE,SAAS,EAAE,cAAc,CAAA;CAC1B;AAED,2DAA2D;AAC3D,MAAM,WAAW,UAAU;IACzB,kEAAkE;IAClE,SAAS,EAAE,aAAa,CAAA;IACxB,4FAA4F;IAC5F,iBAAiB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CACtE;AAmBD;;;;;;;;;GASG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,oBAAoB,EAC5B,OAAO,EAAE,sBAAsB,EAC/B,IAAI,EAAE,UAAU,EAChB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,aAAa,EAAE,CAAC,CA6D1B"}