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.
- package/README.md +15 -6
- package/README.zh.md +14 -6
- package/lib/auth/device-flow.d.ts +64 -0
- package/lib/auth/device-flow.js +176 -0
- package/lib/auth/oauth-flow.js +1 -1
- package/lib/auth/rpc.d.ts +3 -1
- package/lib/auth/store.d.ts +20 -2
- package/lib/auth/store.js +45 -9
- package/lib/client/SubscriptionsSection.d.ts +1 -1
- package/lib/client/SubscriptionsSection.js +46 -4
- package/lib/client/locales.d.ts +8 -0
- package/lib/client/locales.js +8 -0
- package/lib/client.js +94 -4
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +3 -2
- package/lib/index.js +1770 -156
- package/lib/providers/catalog-store.js +15 -0
- package/lib/providers/claude.d.ts +20 -1
- package/lib/providers/claude.js +44 -27
- package/lib/providers/codex.js +52 -8
- package/lib/providers/common.d.ts +32 -1
- package/lib/providers/common.js +48 -1
- package/lib/providers/copilot.d.ts +315 -0
- package/lib/providers/copilot.js +786 -0
- package/lib/providers/grok.d.ts +7 -2
- package/lib/providers/grok.js +46 -18
- package/lib/translate/anthropic.d.ts +47 -6
- package/lib/translate/anthropic.js +135 -20
- package/lib/translate/chat-completions.d.ts +120 -0
- package/lib/translate/chat-completions.js +363 -0
- package/lib/translate/responses.d.ts +49 -5
- package/lib/translate/responses.js +40 -7
- package/package.json +9 -10
|
@@ -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
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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.
|
|
4
|
-
"description": "Use ChatGPT (Codex), Claude,
|
|
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
|
+
}
|