dsh-plugin-subscriptions 0.5.0 → 0.5.1

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,363 @@
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 { CallId, EMPTY_RESPONSE_CODE, LlmError } from '@deepseek-ai/dsh-llm';
9
+ import { parseSse } from './sse.js';
10
+ /** Flatten a tool result's content to plain text for a `tool` message. */
11
+ function toolResultText(block) {
12
+ return block.content.map(part => (part.type === 'text' ? part.text : '')).join('');
13
+ }
14
+ /**
15
+ * Convert harness messages into chat completions `messages`. System-role
16
+ * messages become one leading `system` message; an explicit `system` argument
17
+ * wins over them when both exist. Reasoning blocks are not replayed (matching
18
+ * the Responses translator). Images must arrive pre-resolved; an unresolved
19
+ * ImageBlock is skipped because its bytes are unreachable here. A user message
20
+ * carrying only text collapses to a plain string body (some endpoints still
21
+ * reject content-part arrays); tool results become separate `tool` messages.
22
+ * @param messages - ordered conversation messages with resolved images.
23
+ * @param system - explicit system prompt, which takes precedence.
24
+ * @returns the wire `messages` array.
25
+ */
26
+ export function toChatMessages(messages, system) {
27
+ const out = [];
28
+ const systemTexts = [];
29
+ for (const message of messages) {
30
+ if (message.role === 'system') {
31
+ for (const block of message.content) {
32
+ if (block.type === 'text')
33
+ systemTexts.push(block.text);
34
+ }
35
+ continue;
36
+ }
37
+ if (message.role === 'user') {
38
+ // Tool results ride inside user-role messages; they become their own
39
+ // `tool` messages while ordinary blocks accumulate into one user entry.
40
+ let texts = [];
41
+ let parts = [];
42
+ const flushUser = () => {
43
+ if (parts.length > 0) {
44
+ if (texts.length > 0)
45
+ parts.unshift({ type: 'text', text: texts.join('\n') });
46
+ out.push({ role: 'user', content: parts });
47
+ }
48
+ else if (texts.length > 0) {
49
+ out.push({ role: 'user', content: texts.join('\n') });
50
+ }
51
+ texts = [];
52
+ parts = [];
53
+ };
54
+ for (const block of message.content) {
55
+ switch (block.type) {
56
+ case 'text':
57
+ texts.push(block.text);
58
+ break;
59
+ case 'image':
60
+ if ('dataBase64' in block) {
61
+ parts.push({
62
+ type: 'image_url',
63
+ image_url: { url: `data:${block.mediaType};base64,${block.dataBase64}` },
64
+ });
65
+ }
66
+ break;
67
+ case 'tool-result':
68
+ flushUser();
69
+ out.push({
70
+ role: 'tool',
71
+ tool_call_id: String(block.toolCallId),
72
+ content: toolResultText(block),
73
+ });
74
+ break;
75
+ default:
76
+ break;
77
+ }
78
+ }
79
+ flushUser();
80
+ continue;
81
+ }
82
+ // assistant: text becomes content, tool calls become the tool_calls array.
83
+ const texts = [];
84
+ const toolCalls = [];
85
+ for (const block of message.content) {
86
+ switch (block.type) {
87
+ case 'text':
88
+ texts.push(block.text);
89
+ break;
90
+ case 'tool-call':
91
+ toolCalls.push({
92
+ id: String(block.id),
93
+ type: 'function',
94
+ function: { name: block.name, arguments: block.arguments },
95
+ });
96
+ break;
97
+ default:
98
+ // reasoning (not replayed), unknown blocks.
99
+ break;
100
+ }
101
+ }
102
+ if (texts.length === 0 && toolCalls.length === 0)
103
+ continue;
104
+ out.push({
105
+ role: 'assistant',
106
+ content: texts.join('\n'),
107
+ ...toolCalls.length > 0 ? { tool_calls: toolCalls } : {},
108
+ });
109
+ }
110
+ const systemText = system ?? (systemTexts.length > 0 ? systemTexts.join('\n\n') : undefined);
111
+ if (systemText !== undefined)
112
+ out.unshift({ role: 'system', content: systemText });
113
+ return out;
114
+ }
115
+ /**
116
+ * Map harness tool schemas to chat completions function tools.
117
+ * @param tools - tool schemas from the request.
118
+ * @returns the wire `tools` array.
119
+ */
120
+ export function toChatTools(tools) {
121
+ return tools.map(tool => ({
122
+ type: 'function',
123
+ function: {
124
+ name: tool.name,
125
+ description: tool.description,
126
+ parameters: tool.parameters,
127
+ },
128
+ }));
129
+ }
130
+ /**
131
+ * Map chat completions usage to disjoint harness counts (cached input is
132
+ * subtracted out of `inputTokens` and reported as `cacheReadTokens`).
133
+ * @param usage - wire usage from the terminal chunk.
134
+ * @returns harness token usage.
135
+ */
136
+ export function mapChatCompletionsUsage(usage) {
137
+ const cached = usage.prompt_tokens_details?.cached_tokens;
138
+ const reasoning = usage.completion_tokens_details?.reasoning_tokens;
139
+ return {
140
+ inputTokens: usage.prompt_tokens - (cached ?? 0),
141
+ outputTokens: usage.completion_tokens,
142
+ ...cached !== undefined ? { cacheReadTokens: cached } : {},
143
+ ...reasoning !== undefined ? { reasoningTokens: reasoning } : {},
144
+ };
145
+ }
146
+ /** Assemble the final ContentBlock for one open block. */
147
+ function closeBlock(block) {
148
+ switch (block.kind) {
149
+ case 'text':
150
+ return { type: 'text', text: block.text };
151
+ case 'reasoning':
152
+ return { type: 'reasoning', text: block.text };
153
+ case 'tool-call':
154
+ return {
155
+ type: 'tool-call',
156
+ id: CallId(block.callId),
157
+ name: block.name ?? '',
158
+ arguments: block.text,
159
+ };
160
+ }
161
+ }
162
+ /**
163
+ * Push-model chat completions SSE translator: feed each parsed chunk object
164
+ * to {@link push} and collect the emitted harness StreamChunks. The terminal
165
+ * `finish_reason` chunk closes every block but only ARMS the finish chunk —
166
+ * usage must precede the terminal finish, and where usage lives differs by
167
+ * upstream: OpenAI-style streams send a trailing usage-only chunk
168
+ * (stream_options.include_usage), while Copilot's Gemini models attach a
169
+ * (zero) usage object to EVERY chunk and fold the real usage into the
170
+ * finish chunk itself. A chunk therefore never early-returns on `usage`
171
+ * alone: its deltas are always processed, and the terminal pair is drained
172
+ * when the finish is armed and usage arrived (or when a usage-only chunk
173
+ * follows an armed finish). `flush()` emits whatever remains when the
174
+ * stream's `[DONE]` (or EOF) arrives.
175
+ */
176
+ export class ChatCompletionsStreamTranslator {
177
+ /** Text/reasoning blocks keyed by kind; tool calls keyed by their wire index. */
178
+ blocks = new Map();
179
+ order = [];
180
+ nextIndex = 0;
181
+ sawToolCall = false;
182
+ pendingUsage;
183
+ armedFinish;
184
+ /** Set once the terminal finish chunk was emitted. */
185
+ terminated = false;
186
+ open(key, kind, chunks, callId = '', name) {
187
+ const block = {
188
+ index: this.nextIndex++,
189
+ kind,
190
+ text: '',
191
+ callId,
192
+ ...name === undefined ? {} : { name },
193
+ };
194
+ this.blocks.set(key, block);
195
+ this.order.push(block);
196
+ chunks.push({ type: 'block-start', index: block.index, blockType: kind });
197
+ return block;
198
+ }
199
+ close(key, chunks) {
200
+ const block = this.blocks.get(key);
201
+ if (block === undefined)
202
+ return;
203
+ this.blocks.delete(key);
204
+ chunks.push({ type: 'block-end', index: block.index, block: closeBlock(block) });
205
+ }
206
+ closeAll(chunks) {
207
+ for (const key of [...this.blocks.keys()])
208
+ this.close(key, chunks);
209
+ }
210
+ /** Build the terminal finish chunk for one wire finish reason. */
211
+ finishChunk(finishReason) {
212
+ if (this.order.length === 0) {
213
+ return {
214
+ type: 'finish',
215
+ reason: {
216
+ kind: 'error',
217
+ failure: { message: 'model returned a completed response with no content', code: EMPTY_RESPONSE_CODE },
218
+ },
219
+ };
220
+ }
221
+ switch (finishReason) {
222
+ case 'tool_calls':
223
+ return { type: 'finish', reason: { kind: 'tool-calls' } };
224
+ case 'length':
225
+ return { type: 'finish', reason: { kind: 'max-tokens' } };
226
+ case 'content_filter':
227
+ return {
228
+ type: 'finish',
229
+ reason: {
230
+ kind: 'error',
231
+ failure: { message: 'the response was blocked by the provider content filter', code: 'CONTENT_FILTER' },
232
+ },
233
+ };
234
+ default:
235
+ return { type: 'finish', reason: { kind: this.sawToolCall ? 'tool-calls' : 'stop' } };
236
+ }
237
+ }
238
+ /** Usage, then the armed finish: the only order the harness accepts. */
239
+ drainTerminal(chunks) {
240
+ if (this.pendingUsage !== undefined) {
241
+ chunks.push({ type: 'usage', usage: mapChatCompletionsUsage(this.pendingUsage) });
242
+ this.pendingUsage = undefined;
243
+ }
244
+ if (this.armedFinish !== undefined) {
245
+ chunks.push(this.armedFinish);
246
+ this.armedFinish = undefined;
247
+ this.terminated = true;
248
+ }
249
+ }
250
+ /**
251
+ * Process one parsed chat-completion chunk.
252
+ * @param event - the parsed chunk object.
253
+ * @returns the StreamChunks this event produced (possibly none).
254
+ */
255
+ push(event) {
256
+ if (this.terminated)
257
+ return [];
258
+ const chunks = [];
259
+ const usage = event.usage;
260
+ const hasUsage = usage !== undefined && usage !== null;
261
+ if (hasUsage)
262
+ this.pendingUsage = usage;
263
+ const choice = event.choices?.[0];
264
+ const delta = choice?.delta;
265
+ if (delta !== undefined) {
266
+ if (typeof delta.content === 'string' && delta.content.length > 0) {
267
+ const block = this.blocks.get('content') ?? this.open('content', 'text', chunks);
268
+ block.text += delta.content;
269
+ chunks.push({ type: 'text-delta', index: block.index, text: delta.content });
270
+ }
271
+ const reasoning = typeof delta.reasoning_content === 'string' ? delta.reasoning_content
272
+ : typeof delta.reasoning_text === 'string' ? delta.reasoning_text
273
+ : undefined;
274
+ if (reasoning !== undefined && reasoning.length > 0) {
275
+ const block = this.blocks.get('reasoning') ?? this.open('reasoning', 'reasoning', chunks);
276
+ block.text += reasoning;
277
+ chunks.push({ type: 'reasoning-delta', index: block.index, text: reasoning });
278
+ }
279
+ for (const call of delta.tool_calls ?? []) {
280
+ const key = `call:${String(call.index ?? 0)}`;
281
+ let block = this.blocks.get(key);
282
+ if (block === undefined) {
283
+ this.sawToolCall = true;
284
+ block = this.open(key, 'tool-call', chunks, call.id ?? '', call.function?.name);
285
+ chunks.push({
286
+ type: 'tool-call-delta',
287
+ index: block.index,
288
+ id: CallId(block.callId),
289
+ ...block.name === undefined ? {} : { name: block.name },
290
+ argumentsDelta: '',
291
+ });
292
+ }
293
+ if (call.function?.arguments !== undefined && call.function.arguments.length > 0) {
294
+ block.text += call.function.arguments;
295
+ chunks.push({
296
+ type: 'tool-call-delta',
297
+ index: block.index,
298
+ id: CallId(block.callId),
299
+ argumentsDelta: call.function.arguments,
300
+ });
301
+ }
302
+ }
303
+ }
304
+ if (choice?.finish_reason !== undefined && choice.finish_reason !== null) {
305
+ this.closeAll(chunks);
306
+ // Only arm: usage must be emitted before the terminal finish, and it
307
+ // may still arrive (OpenAI's trailing usage-only chunk) or may have
308
+ // arrived in this very chunk (Gemini folds it in). The drain below or
309
+ // flush() releases the pair.
310
+ if (this.armedFinish === undefined)
311
+ this.armedFinish = this.finishChunk(choice.finish_reason);
312
+ }
313
+ // Drain at the terminal point: this chunk carried usage AND either the
314
+ // finish is armed (usage + finish pair complete — same chunk for Gemini,
315
+ // trailing chunk for OpenAI) or the chunk is usage-only (no choices to
316
+ // process). Mid-stream usage carriers (Gemini's zero-usage deltas) keep
317
+ // their pendingUsage for a later drain; only the final real usage is
318
+ // emitted.
319
+ if (hasUsage && (this.armedFinish !== undefined || choice === undefined)) {
320
+ this.drainTerminal(chunks);
321
+ }
322
+ return chunks;
323
+ }
324
+ /**
325
+ * Emit whatever the stream left pending (`[DONE]` or EOF without a final
326
+ * usage chunk). Safe to call repeatedly.
327
+ * @returns the remaining terminal chunks.
328
+ */
329
+ flush() {
330
+ const chunks = [];
331
+ this.drainTerminal(chunks);
332
+ return chunks;
333
+ }
334
+ }
335
+ /**
336
+ * Consume a chat completions SSE byte stream and yield harness StreamChunks.
337
+ * @param stream - raw response body.
338
+ * @param onActivity - transport-activity callback for the idle watchdog.
339
+ * @returns the chunk stream; throws when the stream ends before any finish chunk.
340
+ */
341
+ export async function* streamChatCompletions(stream, onActivity) {
342
+ const translator = new ChatCompletionsStreamTranslator();
343
+ for await (const sseEvent of parseSse(stream, onActivity)) {
344
+ if (sseEvent.data === '[DONE]') {
345
+ yield* translator.flush();
346
+ return;
347
+ }
348
+ let event;
349
+ try {
350
+ event = JSON.parse(sseEvent.data);
351
+ }
352
+ catch {
353
+ throw new LlmError(`malformed SSE payload: ${sseEvent.data.slice(0, 120)}`, 'MALFORMED_RESPONSE');
354
+ }
355
+ yield* translator.push(event);
356
+ if (translator.terminated)
357
+ return;
358
+ }
359
+ yield* translator.flush();
360
+ if (!translator.terminated) {
361
+ throw new LlmError('chat completions SSE stream ended before a finish chunk', 'STREAM_CLOSED');
362
+ }
363
+ }
@@ -14,17 +14,46 @@ export interface ResponsesRequestInput {
14
14
  /** Responses `input` items in conversation order. */
15
15
  input: Record<string, unknown>[];
16
16
  }
17
+ /**
18
+ * One COMPLETED reasoning output item captured off a response, replayed as
19
+ * the complete item on a later request of the same conversation. The
20
+ * Responses input schema does not treat a reasoning item's `id` or
21
+ * `summary` as optional — a bare `{ type, encrypted_content }` is not a
22
+ * valid input item — so the capture keeps the item's gateway id (as it
23
+ * arrived on the done event), its summary parts, its status, and the
24
+ * encrypted payload. `encrypted_content` is the only required field here:
25
+ * items without one are simply never captured.
26
+ */
27
+ export interface ReasoningReplayItem {
28
+ type: 'reasoning';
29
+ /** The item's gateway id as it arrived on the done event. */
30
+ id?: string;
31
+ /** Summary parts, passed through when the gateway disclosed them. */
32
+ summary?: unknown[];
33
+ /** Item lifecycle status, passed through when present (typically `completed`). */
34
+ status?: string;
35
+ /** Encrypted reasoning payload; the reason the item is worth replaying. */
36
+ encrypted_content: string;
37
+ }
17
38
  /**
18
39
  * Convert harness messages into Responses `instructions` + `input` items.
19
40
  * System-role messages become `instructions`; an explicit `system` argument
20
- * wins over them when both exist. Reasoning blocks are not replayed (v1).
21
- * Images must arrive pre-resolved ({@link TranslatableMessage}); an unresolved
22
- * ImageBlock is skipped because its bytes are unreachable here.
41
+ * wins over them when both exist. Reasoning blocks are never replayed in
42
+ * their text form: a Responses model continuing past a tool call needs its
43
+ * reasoning back as the provider's completed reasoning items (id, summary,
44
+ * and the ENCRYPTED payload), so `reasoningFor` may resolve per-call
45
+ * captured items, replayed ahead of the matching function_call item. Images
46
+ * must arrive pre-resolved
47
+ * ({@link TranslatableMessage}); an unresolved ImageBlock is skipped because
48
+ * its bytes are unreachable here.
23
49
  * @param messages - ordered conversation messages with resolved images.
24
50
  * @param system - explicit system prompt, which takes precedence.
51
+ * @param reasoningFor - resolves one tool call id to the COMPLETED reasoning
52
+ * items captured for it (id, summary, status, encrypted payload), replayed
53
+ * ahead of the matching function_call item, when the adapter kept them.
25
54
  * @returns request fields ready to merge into the request body.
26
55
  */
27
- export declare function toResponsesInput(messages: readonly TranslatableMessage[], system?: string): ResponsesRequestInput;
56
+ export declare function toResponsesInput(messages: readonly TranslatableMessage[], system?: string, reasoningFor?: (callId: string) => readonly ReasoningReplayItem[] | undefined): ResponsesRequestInput;
28
57
  /**
29
58
  * Map harness tool schemas to Responses function tools.
30
59
  * @param tools - tool schemas from the request.
@@ -35,6 +64,12 @@ export declare function toResponsesTools(tools: readonly ToolSchema[]): Record<s
35
64
  export interface ResponsesStreamEvent {
36
65
  type: string;
37
66
  item_id?: string;
67
+ /**
68
+ * Position of the event's item in the response's output array. The spec
69
+ * carries it on output_item/delta events; Copilot's adapter uses it as the
70
+ * stable item correlator when the gateway mints fresh ids per event.
71
+ */
72
+ output_index?: number;
38
73
  content_index?: number;
39
74
  summary_index?: number;
40
75
  delta?: string;
@@ -44,6 +79,12 @@ export interface ResponsesStreamEvent {
44
79
  call_id?: string;
45
80
  name?: string;
46
81
  arguments?: string;
82
+ /** Encrypted reasoning payload, present when the request asked to include it. */
83
+ encrypted_content?: string;
84
+ /** Summary parts of a completed reasoning item, when the gateway disclosed them. */
85
+ summary?: unknown[];
86
+ /** Item lifecycle status (e.g. `completed`), when present. */
87
+ status?: string;
47
88
  content?: Array<{
48
89
  type?: string;
49
90
  text?: string;
@@ -122,6 +163,9 @@ export declare class ResponsesStreamTranslator {
122
163
  * Consume a Responses SSE byte stream and yield harness StreamChunks.
123
164
  * @param stream - raw response body.
124
165
  * @param onActivity - transport-activity callback for the idle watchdog.
166
+ * @param transform - optional per-event rewrite applied before translation
167
+ * (Copilot's gateway mints a fresh item id per event; the adapter rewrites
168
+ * them into stable per-item keys).
125
169
  * @returns the chunk stream; throws when the stream ends before `response.completed`.
126
170
  */
127
- export declare function streamResponses(stream: ReadableStream<Uint8Array>, onActivity?: () => void): AsyncGenerator<StreamChunk>;
171
+ export declare function streamResponses(stream: ReadableStream<Uint8Array>, onActivity?: () => void, transform?: (event: ResponsesStreamEvent) => ResponsesStreamEvent): AsyncGenerator<StreamChunk>;
@@ -13,16 +13,29 @@ function toolResultText(block) {
13
13
  /**
14
14
  * Convert harness messages into Responses `instructions` + `input` items.
15
15
  * System-role messages become `instructions`; an explicit `system` argument
16
- * wins over them when both exist. Reasoning blocks are not replayed (v1).
17
- * Images must arrive pre-resolved ({@link TranslatableMessage}); an unresolved
18
- * ImageBlock is skipped because its bytes are unreachable here.
16
+ * wins over them when both exist. Reasoning blocks are never replayed in
17
+ * their text form: a Responses model continuing past a tool call needs its
18
+ * reasoning back as the provider's completed reasoning items (id, summary,
19
+ * and the ENCRYPTED payload), so `reasoningFor` may resolve per-call
20
+ * captured items, replayed ahead of the matching function_call item. Images
21
+ * must arrive pre-resolved
22
+ * ({@link TranslatableMessage}); an unresolved ImageBlock is skipped because
23
+ * its bytes are unreachable here.
19
24
  * @param messages - ordered conversation messages with resolved images.
20
25
  * @param system - explicit system prompt, which takes precedence.
26
+ * @param reasoningFor - resolves one tool call id to the COMPLETED reasoning
27
+ * items captured for it (id, summary, status, encrypted payload), replayed
28
+ * ahead of the matching function_call item, when the adapter kept them.
21
29
  * @returns request fields ready to merge into the request body.
22
30
  */
23
- export function toResponsesInput(messages, system) {
31
+ export function toResponsesInput(messages, system, reasoningFor) {
24
32
  const input = [];
25
33
  const systemTexts = [];
34
+ // [2026-08-23]-[reasoning models lose their chain of thought across a tool
35
+ // round trip unless the completed reasoning items ride back in; dedupe by
36
+ // ARRAY REFERENCE so parallel calls of one response (which share one array
37
+ // instance) replay the items once, before the first of them]
38
+ let lastReplay;
26
39
  for (const message of messages) {
27
40
  if (message.role === 'system') {
28
41
  for (const block of message.content) {
@@ -44,8 +57,21 @@ export function toResponsesInput(messages, system) {
44
57
  case 'text':
45
58
  content.push({ type: role === 'assistant' ? 'output_text' : 'input_text', text: block.text });
46
59
  break;
47
- case 'tool-call':
60
+ case 'tool-call': {
48
61
  flushMessage();
62
+ const encrypted = reasoningFor?.(String(block.id));
63
+ if (encrypted !== undefined && encrypted !== lastReplay) {
64
+ for (const item of encrypted) {
65
+ input.push({
66
+ type: 'reasoning',
67
+ ...item.id === undefined ? {} : { id: item.id },
68
+ ...item.summary === undefined ? {} : { summary: item.summary },
69
+ ...item.status === undefined ? {} : { status: item.status },
70
+ encrypted_content: item.encrypted_content,
71
+ });
72
+ }
73
+ lastReplay = encrypted;
74
+ }
49
75
  input.push({
50
76
  type: 'function_call',
51
77
  call_id: String(block.id),
@@ -53,6 +79,7 @@ export function toResponsesInput(messages, system) {
53
79
  arguments: block.arguments,
54
80
  });
55
81
  break;
82
+ }
56
83
  case 'tool-result':
57
84
  flushMessage();
58
85
  input.push({
@@ -72,7 +99,8 @@ export function toResponsesInput(messages, system) {
72
99
  // adapter resolves images before translation, so this is skipped.
73
100
  break;
74
101
  default:
75
- // reasoning (not replayed), unknown blocks.
102
+ // reasoning's text form is not replayed (encrypted replay rides
103
+ // reasoningFor), unknown blocks.
76
104
  break;
77
105
  }
78
106
  }
@@ -332,9 +360,12 @@ export class ResponsesStreamTranslator {
332
360
  * Consume a Responses SSE byte stream and yield harness StreamChunks.
333
361
  * @param stream - raw response body.
334
362
  * @param onActivity - transport-activity callback for the idle watchdog.
363
+ * @param transform - optional per-event rewrite applied before translation
364
+ * (Copilot's gateway mints a fresh item id per event; the adapter rewrites
365
+ * them into stable per-item keys).
335
366
  * @returns the chunk stream; throws when the stream ends before `response.completed`.
336
367
  */
337
- export async function* streamResponses(stream, onActivity) {
368
+ export async function* streamResponses(stream, onActivity, transform) {
338
369
  const translator = new ResponsesStreamTranslator();
339
370
  for await (const sseEvent of parseSse(stream, onActivity)) {
340
371
  let event;
@@ -344,6 +375,8 @@ export async function* streamResponses(stream, onActivity) {
344
375
  catch {
345
376
  throw new LlmError(`malformed SSE payload: ${sseEvent.data.slice(0, 120)}`, 'MALFORMED_RESPONSE');
346
377
  }
378
+ if (transform !== undefined)
379
+ event = transform(event);
347
380
  yield* translator.push(event);
348
381
  if (translator.terminated)
349
382
  return;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-plugin-subscriptions",
3
- "version": "0.5.0",
4
- "description": "Use ChatGPT (Codex), Claude, and Grok (X Premium) subscriptions as DeepSeek Harness LLM providers, with OAuth login from the web Settings page",
3
+ "version": "0.5.1",
4
+ "description": "Use ChatGPT (Codex), Claude, Grok (X Premium), and GitHub Copilot subscriptions as DeepSeek Harness LLM providers, with OAuth login from the web Settings page",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -15,7 +15,8 @@
15
15
  "chatgpt",
16
16
  "codex",
17
17
  "claude",
18
- "grok"
18
+ "grok",
19
+ "github-copilot"
19
20
  ],
20
21
  "type": "module",
21
22
  "main": "lib/index.js",
@@ -48,12 +49,6 @@
48
49
  ]
49
50
  }
50
51
  },
51
- "scripts": {
52
- "build": "tsc && tsdown",
53
- "test": "tsc -p tsconfig.test.json && node --test lib-test/test/",
54
- "prepare": "tsdown -c tsdown.prepare.config.ts",
55
- "prepublishOnly": "pnpm build && pnpm test"
56
- },
57
52
  "peerDependencies": {
58
53
  "@deepseek-ai/cordis": "^4.0.1",
59
54
  "@deepseek-ai/dsh-attachment": "^0.1.0-rc.5",
@@ -83,5 +78,9 @@
83
78
  "react": "^18.2.0",
84
79
  "tsdown": "^0.15.0",
85
80
  "typescript": "^5.8.0"
81
+ },
82
+ "scripts": {
83
+ "build": "tsc && tsdown",
84
+ "test": "tsc -p tsconfig.test.json && node --test lib-test/test/"
86
85
  }
87
- }
86
+ }