dsh-subscriptions 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 (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/cordis.patch.yml +12 -0
  4. package/lib/auth/claude-code-creds.d.ts +23 -0
  5. package/lib/auth/claude-code-creds.js +172 -0
  6. package/lib/auth/device-flow.d.ts +55 -0
  7. package/lib/auth/device-flow.js +177 -0
  8. package/lib/auth/jwt.d.ts +10 -0
  9. package/lib/auth/jwt.js +25 -0
  10. package/lib/auth/oauth-flow.d.ts +91 -0
  11. package/lib/auth/oauth-flow.js +227 -0
  12. package/lib/auth/pkce.d.ts +31 -0
  13. package/lib/auth/pkce.js +35 -0
  14. package/lib/auth/rpc.d.ts +209 -0
  15. package/lib/auth/rpc.js +386 -0
  16. package/lib/auth/store.d.ts +196 -0
  17. package/lib/auth/store.js +468 -0
  18. package/lib/client/CursorCard.d.ts +9 -0
  19. package/lib/client/CursorCard.js +113 -0
  20. package/lib/client/ExternalUsageCards.d.ts +9 -0
  21. package/lib/client/ExternalUsageCards.js +72 -0
  22. package/lib/client/ImageGallery.d.ts +54 -0
  23. package/lib/client/ImageGallery.js +112 -0
  24. package/lib/client/ImageGenerateToolview.d.ts +48 -0
  25. package/lib/client/ImageGenerateToolview.js +133 -0
  26. package/lib/client/ProviderAccountManager.d.ts +13 -0
  27. package/lib/client/ProviderAccountManager.js +147 -0
  28. package/lib/client/ProviderModelEditor.d.ts +39 -0
  29. package/lib/client/ProviderModelEditor.js +107 -0
  30. package/lib/client/SpeedSelect.d.ts +71 -0
  31. package/lib/client/SpeedSelect.js +177 -0
  32. package/lib/client/SubscriptionUsageBadge.d.ts +116 -0
  33. package/lib/client/SubscriptionUsageBadge.js +543 -0
  34. package/lib/client/SubscriptionsSection.d.ts +140 -0
  35. package/lib/client/SubscriptionsSection.js +544 -0
  36. package/lib/client/UsageBadgeDisplaySetting.d.ts +7 -0
  37. package/lib/client/UsageBadgeDisplaySetting.js +36 -0
  38. package/lib/client/VideoGenerateToolview.d.ts +42 -0
  39. package/lib/client/VideoGenerateToolview.js +175 -0
  40. package/lib/client/account-preferences.d.ts +24 -0
  41. package/lib/client/account-preferences.js +21 -0
  42. package/lib/client/fast-command.d.ts +2 -0
  43. package/lib/client/fast-command.js +4 -0
  44. package/lib/client/host-contract.d.ts +90 -0
  45. package/lib/client/host-contract.js +101 -0
  46. package/lib/client/host-icons.d.ts +24 -0
  47. package/lib/client/host-icons.js +21 -0
  48. package/lib/client/index.d.ts +27 -0
  49. package/lib/client/index.js +136 -0
  50. package/lib/client/locales.d.ts +297 -0
  51. package/lib/client/locales.js +295 -0
  52. package/lib/client/subscription-card-styles.d.ts +3 -0
  53. package/lib/client/subscription-card-styles.js +53 -0
  54. package/lib/client/subscriptions-rpc.d.ts +14 -0
  55. package/lib/client/subscriptions-rpc.js +34 -0
  56. package/lib/client/usage-badge-preferences.d.ts +11 -0
  57. package/lib/client/usage-badge-preferences.js +41 -0
  58. package/lib/client.js +4472 -0
  59. package/lib/client.js.map +1 -0
  60. package/lib/compat.d.ts +36 -0
  61. package/lib/compat.js +20 -0
  62. package/lib/http.d.ts +6 -0
  63. package/lib/http.js +15 -0
  64. package/lib/index.d.ts +80 -0
  65. package/lib/index.js +14018 -0
  66. package/lib/model-defaults.d.ts +23 -0
  67. package/lib/model-defaults.js +237 -0
  68. package/lib/provider-settings.d.ts +39 -0
  69. package/lib/provider-settings.js +151 -0
  70. package/lib/providers/account-preferences.d.ts +41 -0
  71. package/lib/providers/account-preferences.js +153 -0
  72. package/lib/providers/accounts.d.ts +112 -0
  73. package/lib/providers/accounts.js +142 -0
  74. package/lib/providers/antigravity-oauth-client.d.ts +7 -0
  75. package/lib/providers/antigravity-oauth-client.js +7 -0
  76. package/lib/providers/antigravity.d.ts +110 -0
  77. package/lib/providers/antigravity.js +516 -0
  78. package/lib/providers/catalog-store.d.ts +48 -0
  79. package/lib/providers/catalog-store.js +243 -0
  80. package/lib/providers/claude-cli-version.d.ts +15 -0
  81. package/lib/providers/claude-cli-version.js +24 -0
  82. package/lib/providers/claude.d.ts +181 -0
  83. package/lib/providers/claude.js +680 -0
  84. package/lib/providers/codex-client-version.d.ts +8 -0
  85. package/lib/providers/codex-client-version.js +17 -0
  86. package/lib/providers/codex-search.d.ts +29 -0
  87. package/lib/providers/codex-search.js +198 -0
  88. package/lib/providers/codex.d.ts +229 -0
  89. package/lib/providers/codex.js +821 -0
  90. package/lib/providers/common.d.ts +389 -0
  91. package/lib/providers/common.js +656 -0
  92. package/lib/providers/copilot.d.ts +344 -0
  93. package/lib/providers/copilot.js +888 -0
  94. package/lib/providers/cursor-adapter.d.ts +15 -0
  95. package/lib/providers/cursor-adapter.js +39 -0
  96. package/lib/providers/cursor-auth.d.ts +43 -0
  97. package/lib/providers/cursor-auth.js +169 -0
  98. package/lib/providers/cursor-model-settings.d.ts +15 -0
  99. package/lib/providers/cursor-model-settings.js +51 -0
  100. package/lib/providers/cursor-usage.d.ts +9 -0
  101. package/lib/providers/cursor-usage.js +112 -0
  102. package/lib/providers/external-usage-controller.d.ts +19 -0
  103. package/lib/providers/external-usage-controller.js +32 -0
  104. package/lib/providers/external-usage.d.ts +8 -0
  105. package/lib/providers/external-usage.js +82 -0
  106. package/lib/providers/grok.d.ts +205 -0
  107. package/lib/providers/grok.js +713 -0
  108. package/lib/providers/image-pool.d.ts +30 -0
  109. package/lib/providers/image-pool.js +89 -0
  110. package/lib/providers/npm-cli-version.d.ts +56 -0
  111. package/lib/providers/npm-cli-version.js +98 -0
  112. package/lib/providers/pool-family.d.ts +56 -0
  113. package/lib/providers/pool-family.js +45 -0
  114. package/lib/providers/pool-health.d.ts +74 -0
  115. package/lib/providers/pool-health.js +150 -0
  116. package/lib/providers/pool-usage.d.ts +86 -0
  117. package/lib/providers/pool-usage.js +223 -0
  118. package/lib/providers/pool.d.ts +108 -0
  119. package/lib/providers/pool.js +368 -0
  120. package/lib/providers/rate-limit.d.ts +192 -0
  121. package/lib/providers/rate-limit.js +338 -0
  122. package/lib/tools/image-generate.d.ts +128 -0
  123. package/lib/tools/image-generate.js +438 -0
  124. package/lib/tools/registration.d.ts +15 -0
  125. package/lib/tools/registration.js +24 -0
  126. package/lib/tools/video-generate.d.ts +89 -0
  127. package/lib/tools/video-generate.js +257 -0
  128. package/lib/tools/x-search.d.ts +58 -0
  129. package/lib/tools/x-search.js +197 -0
  130. package/lib/translate/anthropic.d.ts +161 -0
  131. package/lib/translate/anthropic.js +515 -0
  132. package/lib/translate/antigravity-schema.d.ts +4 -0
  133. package/lib/translate/antigravity-schema.js +145 -0
  134. package/lib/translate/antigravity-thinking.d.ts +5 -0
  135. package/lib/translate/antigravity-thinking.js +44 -0
  136. package/lib/translate/antigravity.d.ts +114 -0
  137. package/lib/translate/antigravity.js +366 -0
  138. package/lib/translate/chat-completions.d.ts +120 -0
  139. package/lib/translate/chat-completions.js +382 -0
  140. package/lib/translate/resolved.d.ts +80 -0
  141. package/lib/translate/resolved.js +118 -0
  142. package/lib/translate/responses.d.ts +179 -0
  143. package/lib/translate/responses.js +408 -0
  144. package/lib/translate/sse.d.ts +21 -0
  145. package/lib/translate/sse.js +56 -0
  146. package/package.json +114 -0
  147. package/vendor/cursor/LICENSE +21 -0
  148. package/vendor/cursor/index.d.ts +18 -0
  149. package/vendor/cursor/index.js +3574 -0
  150. package/vendor/cursor/proto.js +254 -0
@@ -0,0 +1,382 @@
1
+ /**
2
+ * Translate between the harness message vocabulary and the OpenAI chat
3
+ * completions wire format the Copilot provider speaks: request message/tool
4
+ * assembly and a push-model SSE-chunk → StreamChunk state machine
5
+ * ({@link ChatCompletionsStreamTranslator}) mirroring the Responses
6
+ * translator, so tests need no streams.
7
+ */
8
+ import { EMPTY_RESPONSE_CODE, LlmError } from '@deepseek-ai/dsh-llm';
9
+ import { ToolCallId } from '../compat.js';
10
+ import { parseSse } from './sse.js';
11
+ import { withToolResultImages } from './resolved.js';
12
+ /** Flatten a tool result's content to plain text for a `tool` message. */
13
+ function toolResultText(block) {
14
+ return block.content.map(part => (part.type === 'text' ? part.text : '')).join('');
15
+ }
16
+ /**
17
+ * Convert harness messages into chat completions `messages`. System-role
18
+ * messages become one leading `system` message; an explicit `system` argument
19
+ * wins over them when both exist. Reasoning blocks are not replayed (matching
20
+ * the Responses translator). Images must arrive pre-resolved; an unresolved
21
+ * ImageBlock is skipped because its bytes are unreachable here. A user message
22
+ * carrying only text collapses to a plain string body (some endpoints still
23
+ * reject content-part arrays); tool results become separate `tool` messages.
24
+ * @param messages - ordered conversation messages with resolved images.
25
+ * @param system - explicit system prompt, which takes precedence.
26
+ * @returns the wire `messages` array.
27
+ */
28
+ export function toChatMessages(messages, system) {
29
+ const out = [];
30
+ const systemTexts = [];
31
+ for (const message of withToolResultImages(messages)) {
32
+ if (message.role === 'system') {
33
+ for (const block of message.content) {
34
+ if (block.type === 'text')
35
+ systemTexts.push(block.text);
36
+ }
37
+ continue;
38
+ }
39
+ if (message.role === 'tool') {
40
+ const callId = message.toolCallId ?? message.tool_call_id
41
+ ?? (message.source?.kind === 'tool' ? String(message.source.callId) : undefined);
42
+ if (callId === undefined)
43
+ throw new LlmError('tool result has no call id', 'INVALID_REQUEST');
44
+ out.push({ role: 'tool', tool_call_id: callId, content: message.content.map(block => block.type === 'text' ? block.text : '').join('') });
45
+ continue;
46
+ }
47
+ if (message.role === 'developer') {
48
+ const content = message.content.filter(block => block.type === 'text').map(block => block.text).join('');
49
+ if (content.length > 0)
50
+ out.push({ role: 'developer', content });
51
+ continue;
52
+ }
53
+ if (message.role === 'user') {
54
+ // Tool results ride inside user-role messages; they become their own
55
+ // `tool` messages while ordinary blocks accumulate into one user entry.
56
+ let texts = [];
57
+ let parts = [];
58
+ const flushUser = () => {
59
+ if (parts.length > 0) {
60
+ if (texts.length > 0)
61
+ parts.unshift({ type: 'text', text: texts.join('\n') });
62
+ out.push({ role: 'user', content: parts });
63
+ }
64
+ else if (texts.length > 0) {
65
+ out.push({ role: 'user', content: texts.join('\n') });
66
+ }
67
+ texts = [];
68
+ parts = [];
69
+ };
70
+ for (const block of message.content) {
71
+ switch (block.type) {
72
+ case 'text':
73
+ if (parts.length > 0)
74
+ parts.push({ type: 'text', text: block.text });
75
+ else
76
+ texts.push(block.text);
77
+ break;
78
+ case 'image':
79
+ if ('dataBase64' in block) {
80
+ parts.push({
81
+ type: 'image_url',
82
+ image_url: { url: `data:${block.mediaType};base64,${block.dataBase64}` },
83
+ });
84
+ }
85
+ break;
86
+ case 'tool-result':
87
+ flushUser();
88
+ out.push({
89
+ role: 'tool',
90
+ tool_call_id: String(block.toolCallId),
91
+ content: toolResultText(block),
92
+ });
93
+ break;
94
+ default:
95
+ break;
96
+ }
97
+ }
98
+ flushUser();
99
+ continue;
100
+ }
101
+ // assistant: text becomes content, tool calls become the tool_calls array.
102
+ const texts = [];
103
+ const toolCalls = [];
104
+ for (const block of message.content) {
105
+ switch (block.type) {
106
+ case 'text':
107
+ texts.push(block.text);
108
+ break;
109
+ case 'tool-call':
110
+ toolCalls.push({
111
+ id: String(block.id),
112
+ type: 'function',
113
+ function: { name: block.name, arguments: block.arguments },
114
+ });
115
+ break;
116
+ default:
117
+ // reasoning (not replayed), unknown blocks.
118
+ break;
119
+ }
120
+ }
121
+ if (texts.length === 0 && toolCalls.length === 0)
122
+ continue;
123
+ out.push({
124
+ role: 'assistant',
125
+ content: texts.join('\n'),
126
+ ...toolCalls.length > 0 ? { tool_calls: toolCalls } : {},
127
+ });
128
+ }
129
+ const systemText = system ?? (systemTexts.length > 0 ? systemTexts.join('\n\n') : undefined);
130
+ if (systemText !== undefined)
131
+ out.unshift({ role: 'system', content: systemText });
132
+ return out;
133
+ }
134
+ /**
135
+ * Map harness tool schemas to chat completions function tools.
136
+ * @param tools - tool schemas from the request.
137
+ * @returns the wire `tools` array.
138
+ */
139
+ export function toChatTools(tools) {
140
+ return tools.map(tool => ({
141
+ type: 'function',
142
+ function: {
143
+ name: tool.name,
144
+ description: tool.description,
145
+ parameters: tool.parameters,
146
+ },
147
+ }));
148
+ }
149
+ /**
150
+ * Map chat completions usage to disjoint harness counts (cached input is
151
+ * subtracted out of `inputTokens` and reported as `cacheReadTokens`).
152
+ * @param usage - wire usage from the terminal chunk.
153
+ * @returns harness token usage.
154
+ */
155
+ export function mapChatCompletionsUsage(usage) {
156
+ const cached = usage.prompt_tokens_details?.cached_tokens;
157
+ const reasoning = usage.completion_tokens_details?.reasoning_tokens;
158
+ return {
159
+ inputTokens: usage.prompt_tokens - (cached ?? 0),
160
+ outputTokens: usage.completion_tokens,
161
+ ...cached !== undefined ? { cacheReadTokens: cached } : {},
162
+ ...reasoning !== undefined ? { reasoningTokens: reasoning } : {},
163
+ };
164
+ }
165
+ /** Assemble the final ContentBlock for one open block. */
166
+ function closeBlock(block) {
167
+ switch (block.kind) {
168
+ case 'text':
169
+ return { type: 'text', text: block.text };
170
+ case 'reasoning':
171
+ return { type: 'reasoning', text: block.text };
172
+ case 'tool-call':
173
+ return {
174
+ type: 'tool-call',
175
+ id: ToolCallId(block.callId),
176
+ name: block.name ?? '',
177
+ arguments: block.text,
178
+ };
179
+ }
180
+ }
181
+ /**
182
+ * Push-model chat completions SSE translator: feed each parsed chunk object
183
+ * to {@link push} and collect the emitted harness StreamChunks. The terminal
184
+ * `finish_reason` chunk closes every block but only ARMS the finish chunk —
185
+ * usage must precede the terminal finish, and where usage lives differs by
186
+ * upstream: OpenAI-style streams send a trailing usage-only chunk
187
+ * (stream_options.include_usage), while Copilot's Gemini models attach a
188
+ * (zero) usage object to EVERY chunk and fold the real usage into the
189
+ * finish chunk itself. A chunk therefore never early-returns on `usage`
190
+ * alone: its deltas are always processed, and the terminal pair is drained
191
+ * when the finish is armed and usage arrived (or when a usage-only chunk
192
+ * follows an armed finish). `flush()` emits whatever remains when the
193
+ * stream's `[DONE]` (or EOF) arrives.
194
+ */
195
+ export class ChatCompletionsStreamTranslator {
196
+ /** Text/reasoning blocks keyed by kind; tool calls keyed by their wire index. */
197
+ blocks = new Map();
198
+ order = [];
199
+ nextIndex = 0;
200
+ sawToolCall = false;
201
+ pendingUsage;
202
+ armedFinish;
203
+ /** Set once the terminal finish chunk was emitted. */
204
+ terminated = false;
205
+ open(key, kind, chunks, callId = '', name) {
206
+ const block = {
207
+ index: this.nextIndex++,
208
+ kind,
209
+ text: '',
210
+ callId,
211
+ ...name === undefined ? {} : { name },
212
+ };
213
+ this.blocks.set(key, block);
214
+ this.order.push(block);
215
+ chunks.push({ type: 'block-start', index: block.index, blockType: kind });
216
+ return block;
217
+ }
218
+ close(key, chunks) {
219
+ const block = this.blocks.get(key);
220
+ if (block === undefined)
221
+ return;
222
+ this.blocks.delete(key);
223
+ chunks.push({ type: 'block-end', index: block.index, block: closeBlock(block) });
224
+ }
225
+ closeAll(chunks) {
226
+ for (const key of [...this.blocks.keys()])
227
+ this.close(key, chunks);
228
+ }
229
+ /** Build the terminal finish chunk for one wire finish reason. */
230
+ finishChunk(finishReason) {
231
+ if (this.order.length === 0) {
232
+ return {
233
+ type: 'finish',
234
+ reason: {
235
+ kind: 'error',
236
+ failure: { message: 'model returned a completed response with no content', code: EMPTY_RESPONSE_CODE },
237
+ },
238
+ };
239
+ }
240
+ switch (finishReason) {
241
+ case 'tool_calls':
242
+ return { type: 'finish', reason: { kind: 'tool-calls' } };
243
+ case 'length':
244
+ return { type: 'finish', reason: { kind: 'max-tokens' } };
245
+ case 'content_filter':
246
+ return {
247
+ type: 'finish',
248
+ reason: {
249
+ kind: 'error',
250
+ failure: { message: 'the response was blocked by the provider content filter', code: 'CONTENT_FILTER' },
251
+ },
252
+ };
253
+ default:
254
+ return { type: 'finish', reason: { kind: this.sawToolCall ? 'tool-calls' : 'stop' } };
255
+ }
256
+ }
257
+ /** Usage, then the armed finish: the only order the harness accepts. */
258
+ drainTerminal(chunks) {
259
+ if (this.pendingUsage !== undefined) {
260
+ chunks.push({ type: 'usage', usage: mapChatCompletionsUsage(this.pendingUsage) });
261
+ this.pendingUsage = undefined;
262
+ }
263
+ if (this.armedFinish !== undefined) {
264
+ chunks.push(this.armedFinish);
265
+ this.armedFinish = undefined;
266
+ this.terminated = true;
267
+ }
268
+ }
269
+ /**
270
+ * Process one parsed chat-completion chunk.
271
+ * @param event - the parsed chunk object.
272
+ * @returns the StreamChunks this event produced (possibly none).
273
+ */
274
+ push(event) {
275
+ if (this.terminated)
276
+ return [];
277
+ const chunks = [];
278
+ const usage = event.usage;
279
+ const hasUsage = usage !== undefined && usage !== null;
280
+ if (hasUsage)
281
+ this.pendingUsage = usage;
282
+ const choice = event.choices?.[0];
283
+ const delta = choice?.delta;
284
+ if (delta !== undefined) {
285
+ if (typeof delta.content === 'string' && delta.content.length > 0) {
286
+ const block = this.blocks.get('content') ?? this.open('content', 'text', chunks);
287
+ block.text += delta.content;
288
+ chunks.push({ type: 'text-delta', index: block.index, text: delta.content });
289
+ }
290
+ const reasoning = typeof delta.reasoning_content === 'string' ? delta.reasoning_content
291
+ : typeof delta.reasoning_text === 'string' ? delta.reasoning_text
292
+ : undefined;
293
+ if (reasoning !== undefined && reasoning.length > 0) {
294
+ const block = this.blocks.get('reasoning') ?? this.open('reasoning', 'reasoning', chunks);
295
+ block.text += reasoning;
296
+ chunks.push({ type: 'reasoning-delta', index: block.index, text: reasoning });
297
+ }
298
+ for (const call of delta.tool_calls ?? []) {
299
+ const key = `call:${String(call.index ?? 0)}`;
300
+ let block = this.blocks.get(key);
301
+ if (block === undefined) {
302
+ this.sawToolCall = true;
303
+ block = this.open(key, 'tool-call', chunks, call.id ?? '', call.function?.name);
304
+ chunks.push({
305
+ type: 'tool-call-delta',
306
+ index: block.index,
307
+ id: ToolCallId(block.callId),
308
+ ...block.name === undefined ? {} : { name: block.name },
309
+ argumentsDelta: '',
310
+ });
311
+ }
312
+ if (call.function?.arguments !== undefined && call.function.arguments.length > 0) {
313
+ block.text += call.function.arguments;
314
+ chunks.push({
315
+ type: 'tool-call-delta',
316
+ index: block.index,
317
+ id: ToolCallId(block.callId),
318
+ argumentsDelta: call.function.arguments,
319
+ });
320
+ }
321
+ }
322
+ }
323
+ if (choice?.finish_reason !== undefined && choice.finish_reason !== null) {
324
+ this.closeAll(chunks);
325
+ // Only arm: usage must be emitted before the terminal finish, and it
326
+ // may still arrive (OpenAI's trailing usage-only chunk) or may have
327
+ // arrived in this very chunk (Gemini folds it in). The drain below or
328
+ // flush() releases the pair.
329
+ if (this.armedFinish === undefined)
330
+ this.armedFinish = this.finishChunk(choice.finish_reason);
331
+ }
332
+ // Drain at the terminal point: this chunk carried usage AND either the
333
+ // finish is armed (usage + finish pair complete — same chunk for Gemini,
334
+ // trailing chunk for OpenAI) or the chunk is usage-only (no choices to
335
+ // process). Mid-stream usage carriers (Gemini's zero-usage deltas) keep
336
+ // their pendingUsage for a later drain; only the final real usage is
337
+ // emitted.
338
+ if (hasUsage && (this.armedFinish !== undefined || choice === undefined)) {
339
+ this.drainTerminal(chunks);
340
+ }
341
+ return chunks;
342
+ }
343
+ /**
344
+ * Emit whatever the stream left pending (`[DONE]` or EOF without a final
345
+ * usage chunk). Safe to call repeatedly.
346
+ * @returns the remaining terminal chunks.
347
+ */
348
+ flush() {
349
+ const chunks = [];
350
+ this.drainTerminal(chunks);
351
+ return chunks;
352
+ }
353
+ }
354
+ /**
355
+ * Consume a chat completions SSE byte stream and yield harness StreamChunks.
356
+ * @param stream - raw response body.
357
+ * @param onActivity - transport-activity callback for the idle watchdog.
358
+ * @returns the chunk stream; throws when the stream ends before any finish chunk.
359
+ */
360
+ export async function* streamChatCompletions(stream, onActivity) {
361
+ const translator = new ChatCompletionsStreamTranslator();
362
+ for await (const sseEvent of parseSse(stream, onActivity)) {
363
+ if (sseEvent.data === '[DONE]') {
364
+ yield* translator.flush();
365
+ return;
366
+ }
367
+ let event;
368
+ try {
369
+ event = JSON.parse(sseEvent.data);
370
+ }
371
+ catch {
372
+ throw new LlmError(`malformed SSE payload: ${sseEvent.data.slice(0, 120)}`, 'MALFORMED_RESPONSE');
373
+ }
374
+ yield* translator.push(event);
375
+ if (translator.terminated)
376
+ return;
377
+ }
378
+ yield* translator.flush();
379
+ if (!translator.terminated) {
380
+ throw new LlmError('chat completions SSE stream ended before a finish chunk', 'STREAM_CLOSED');
381
+ }
382
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Resolved-image plumbing for the wire translators. ImageBlocks carry only an
3
+ * attachment reference; the bytes live in the attachment service, which is
4
+ * async I/O. Adapters resolve images BEFORE calling the (pure, synchronous)
5
+ * translators, so the translators see {@link ResolvedImagePart}s with inline
6
+ * base64 data.
7
+ */
8
+ import type { ContentBlock, Message, RequestMessage } from '@deepseek-ai/dsh-llm';
9
+ import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
10
+ /** An image block with its bytes resolved to inline base64 for the wire. */
11
+ export interface ResolvedImagePart {
12
+ type: 'image';
13
+ /** MIME type verified by the attachment service (e.g. `image/png`). */
14
+ mediaType: string;
15
+ /** Base64-encoded image bytes. */
16
+ dataBase64: string;
17
+ }
18
+ /** Translator input block: a harness block, with images pre-resolved. */
19
+ export type TranslatableBlock = ContentBlock | ResolvedImagePart | ResolvedToolResultBlock;
20
+ /** Tool results may themselves carry attachment-backed images. */
21
+ export interface ResolvedToolResultBlock {
22
+ type: 'tool-result';
23
+ toolCallId: string;
24
+ isError?: boolean;
25
+ content: readonly TranslatableBlock[];
26
+ }
27
+ /**
28
+ * Wires with text-only tool outputs receive images in a following user turn.
29
+ * Defer that turn until all consecutive user messages have been processed:
30
+ * parallel tool results can arrive in separate harness messages, and a user
31
+ * image message must not interrupt their tool-call/output pairing.
32
+ */
33
+ export declare function withToolResultImages(messages: readonly TranslatableMessage[]): TranslatableMessage[];
34
+ /** Translator input message: role plus resolved blocks. */
35
+ export interface TranslatableMessage {
36
+ role: 'system' | 'developer' | 'user' | 'assistant' | 'tool';
37
+ content: readonly TranslatableBlock[];
38
+ /** First-class tool result correlation in current harness messages. */
39
+ toolCallId?: string;
40
+ /** Chat Completions correlation in imported histories. */
41
+ tool_call_id?: string;
42
+ isError?: boolean;
43
+ /** Preserved for adapters whose provider-private replay metadata is required. */
44
+ source?: Message['source'];
45
+ }
46
+ /** A route's cap on outgoing image size; stored attachments are never changed. */
47
+ export interface ImageRequestLimit {
48
+ /** Longest edge in pixels an image may be sent at. */
49
+ maxEdge: number;
50
+ /** Encoded-byte target before base64 expansion. */
51
+ maxBytes: number;
52
+ }
53
+ /**
54
+ * The request projection for one oversized image, or undefined when it fits.
55
+ * Hosts before DSH 0.1.7 read a pixel budget (`maxPixels`), later ones the
56
+ * target edges (`width`/`height`); each validates only its own fields, so
57
+ * one projection carries both.
58
+ */
59
+ export declare function imageRequestTarget(ref: {
60
+ width: number;
61
+ height: number;
62
+ }, limit: ImageRequestLimit): {
63
+ width: number;
64
+ height: number;
65
+ maxPixels: number;
66
+ maxBytes: number;
67
+ } | undefined;
68
+ /**
69
+ * Resolve every ImageBlock's attachment reference to inline base64 bytes.
70
+ * Messages without images pass through unchanged. A request carrying an image
71
+ * with no attachment service available fails loudly rather than silently
72
+ * dropping the image.
73
+ * @param messages - the request's conversation messages.
74
+ * @param attachments - the deployment's attachment service, when mounted.
75
+ * @param signal - cancellation for the storage reads.
76
+ * @param limit - the route's outgoing image cap; oversized images are sent
77
+ * downscaled while the stored attachment and history stay untouched.
78
+ * @returns the same messages with image blocks resolved for the translators.
79
+ */
80
+ export declare function resolveImages(messages: readonly RequestMessage[], attachments: AttachmentStore | undefined, signal?: AbortSignal, limit?: ImageRequestLimit): Promise<readonly TranslatableMessage[]>;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Resolved-image plumbing for the wire translators. ImageBlocks carry only an
3
+ * attachment reference; the bytes live in the attachment service, which is
4
+ * async I/O. Adapters resolve images BEFORE calling the (pure, synchronous)
5
+ * translators, so the translators see {@link ResolvedImagePart}s with inline
6
+ * base64 data.
7
+ */
8
+ import { LlmError } from '@deepseek-ai/dsh-llm';
9
+ /**
10
+ * Wires with text-only tool outputs receive images in a following user turn.
11
+ * Defer that turn until all consecutive user messages have been processed:
12
+ * parallel tool results can arrive in separate harness messages, and a user
13
+ * image message must not interrupt their tool-call/output pairing.
14
+ */
15
+ export function withToolResultImages(messages) {
16
+ const out = [];
17
+ let images = [];
18
+ const flush = () => {
19
+ if (images.length > 0)
20
+ out.push({ role: 'user', content: images });
21
+ images = [];
22
+ };
23
+ for (const message of messages) {
24
+ if (message.role === 'assistant')
25
+ flush();
26
+ out.push(message);
27
+ if (message.role === 'tool') {
28
+ const parts = message.content.filter((part) => part.type === 'image' && 'dataBase64' in part);
29
+ if (parts.length > 0) {
30
+ images.push({ type: 'text', text: `Images from tool result ${String(message.toolCallId)}:` }, ...parts);
31
+ }
32
+ }
33
+ for (const block of message.content) {
34
+ if (block.type !== 'tool-result')
35
+ continue;
36
+ const parts = block.content.filter((part) => part.type === 'image' && 'dataBase64' in part);
37
+ if (parts.length > 0) {
38
+ images.push({ type: 'text', text: `Images from tool result ${String(block.toolCallId)}:` }, ...parts);
39
+ }
40
+ }
41
+ }
42
+ flush();
43
+ return out;
44
+ }
45
+ /**
46
+ * The request projection for one oversized image, or undefined when it fits.
47
+ * Hosts before DSH 0.1.7 read a pixel budget (`maxPixels`), later ones the
48
+ * target edges (`width`/`height`); each validates only its own fields, so
49
+ * one projection carries both.
50
+ */
51
+ export function imageRequestTarget(ref, limit) {
52
+ const longEdge = Math.max(ref.width, ref.height);
53
+ if (!(longEdge > limit.maxEdge))
54
+ return undefined;
55
+ const short = (edge) => Math.max(1, Math.floor(edge * limit.maxEdge / longEdge));
56
+ const width = ref.width >= ref.height ? limit.maxEdge : short(ref.width);
57
+ const height = ref.width >= ref.height ? short(ref.height) : limit.maxEdge;
58
+ return { width, height, maxPixels: width * height, maxBytes: limit.maxBytes };
59
+ }
60
+ /**
61
+ * Resolve every ImageBlock's attachment reference to inline base64 bytes.
62
+ * Messages without images pass through unchanged. A request carrying an image
63
+ * with no attachment service available fails loudly rather than silently
64
+ * dropping the image.
65
+ * @param messages - the request's conversation messages.
66
+ * @param attachments - the deployment's attachment service, when mounted.
67
+ * @param signal - cancellation for the storage reads.
68
+ * @param limit - the route's outgoing image cap; oversized images are sent
69
+ * downscaled while the stored attachment and history stay untouched.
70
+ * @returns the same messages with image blocks resolved for the translators.
71
+ */
72
+ export async function resolveImages(messages, attachments, signal, limit) {
73
+ const hasImage = messages.some(message => message.content.some(block => block.type === 'image'));
74
+ if (hasImage && attachments === undefined) {
75
+ throw new LlmError('dsh-subscriptions: the request carries an image but no attachments service is mounted; '
76
+ + 'image input requires the harness attachment store', 'UNSUPPORTED');
77
+ }
78
+ const readForRequest = async (ref) => {
79
+ if (attachments === undefined)
80
+ throw new Error('attachments service is unavailable');
81
+ const target = limit === undefined ? undefined : imageRequestTarget(ref, limit);
82
+ if (target !== undefined) {
83
+ try {
84
+ const version = await attachments.readImageRequest(ref, target, signal);
85
+ return { data: version.data, mediaType: version.mediaType, ref: version.attachment };
86
+ }
87
+ catch (error) {
88
+ // A host that cannot derive request images keeps sending the stored bytes, as before.
89
+ if (signal?.aborted)
90
+ throw error;
91
+ }
92
+ }
93
+ const stored = await attachments.readImage(ref, signal);
94
+ return { data: stored.data, mediaType: stored.ref.mediaType, ref: stored.ref };
95
+ };
96
+ const resolveBlock = async (block) => {
97
+ if (block.type !== 'image')
98
+ return [block];
99
+ const { data, mediaType: sentType, ref } = await readForRequest(block.attachment);
100
+ const { attachmentId, mediaType, bytes, width, height, name } = ref;
101
+ return [{
102
+ type: 'image',
103
+ mediaType: sentType,
104
+ dataBase64: Buffer.from(data).toString('base64'),
105
+ }, {
106
+ type: 'text',
107
+ text: `Image reference (for image_generate.referenceImages): ${JSON.stringify({
108
+ attachmentId, mediaType, bytes, width, height, ...name === undefined ? {} : { name },
109
+ })}`,
110
+ }];
111
+ };
112
+ return Promise.all(messages.map(async (message) => ({
113
+ role: message.role,
114
+ ...message.source === undefined ? {} : { source: message.source },
115
+ ...message.role === 'tool' ? { toolCallId: message.toolCallId, isError: message.isError } : {},
116
+ content: (await Promise.all(message.content.map(resolveBlock))).flat(),
117
+ })));
118
+ }