dsh-plugin-subscription-oauth 0.0.0-stage → 0.2.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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +413 -2
  3. package/cordis.patch.yml +13 -0
  4. package/lib/auth/device-flow.d.ts +112 -0
  5. package/lib/auth/device-flow.js +319 -0
  6. package/lib/auth/device-id.d.ts +31 -0
  7. package/lib/auth/device-id.js +58 -0
  8. package/lib/auth/flow.d.ts +33 -0
  9. package/lib/auth/flow.js +12 -0
  10. package/lib/auth/glm-flow.d.ts +99 -0
  11. package/lib/auth/glm-flow.js +194 -0
  12. package/lib/auth/headers.d.ts +61 -0
  13. package/lib/auth/headers.js +106 -0
  14. package/lib/auth/pkce-flow.d.ts +78 -0
  15. package/lib/auth/pkce-flow.js +205 -0
  16. package/lib/auth/provider-controller.d.ts +241 -0
  17. package/lib/auth/provider-controller.js +502 -0
  18. package/lib/auth/registry.d.ts +91 -0
  19. package/lib/auth/registry.js +149 -0
  20. package/lib/auth/rpc.d.ts +31 -0
  21. package/lib/auth/rpc.js +200 -0
  22. package/lib/auth/store.d.ts +99 -0
  23. package/lib/auth/store.js +213 -0
  24. package/lib/auth/tokens.d.ts +76 -0
  25. package/lib/auth/tokens.js +103 -0
  26. package/lib/client/SubscriptionsSection.d.ts +34 -0
  27. package/lib/client/SubscriptionsSection.js +742 -0
  28. package/lib/client/index.d.ts +36 -0
  29. package/lib/client/index.js +47 -0
  30. package/lib/client/locales.d.ts +187 -0
  31. package/lib/client/locales.js +185 -0
  32. package/lib/client/styles.d.ts +18 -0
  33. package/lib/client/styles.js +257 -0
  34. package/lib/client/subscriptions-rpc.d.ts +24 -0
  35. package/lib/client/subscriptions-rpc.js +44 -0
  36. package/lib/client.js +2078 -0
  37. package/lib/client.js.map +1 -0
  38. package/lib/constants.d.ts +79 -0
  39. package/lib/constants.js +91 -0
  40. package/lib/errors.d.ts +28 -0
  41. package/lib/errors.js +34 -0
  42. package/lib/http.d.ts +24 -0
  43. package/lib/http.js +98 -0
  44. package/lib/index.d.ts +57 -0
  45. package/lib/index.js +185 -0
  46. package/lib/protocol.d.ts +316 -0
  47. package/lib/protocol.js +29 -0
  48. package/lib/providers/antigravity-catalog.d.ts +53 -0
  49. package/lib/providers/antigravity-catalog.js +269 -0
  50. package/lib/providers/antigravity.d.ts +331 -0
  51. package/lib/providers/antigravity.js +821 -0
  52. package/lib/providers/auto.d.ts +151 -0
  53. package/lib/providers/auto.js +351 -0
  54. package/lib/providers/catalog.d.ts +69 -0
  55. package/lib/providers/catalog.js +78 -0
  56. package/lib/providers/common.d.ts +51 -0
  57. package/lib/providers/common.js +169 -0
  58. package/lib/providers/glm-provider.d.ts +41 -0
  59. package/lib/providers/glm-provider.js +165 -0
  60. package/lib/providers/glm.d.ts +159 -0
  61. package/lib/providers/glm.js +445 -0
  62. package/lib/providers/grok.d.ts +224 -0
  63. package/lib/providers/grok.js +700 -0
  64. package/lib/providers/kimi-provider.d.ts +45 -0
  65. package/lib/providers/kimi-provider.js +148 -0
  66. package/lib/providers/kimi.d.ts +151 -0
  67. package/lib/providers/kimi.js +400 -0
  68. package/lib/providers/responses-stream.d.ts +58 -0
  69. package/lib/providers/responses-stream.js +115 -0
  70. package/lib/providers/usage.d.ts +65 -0
  71. package/lib/providers/usage.js +309 -0
  72. package/lib/settings.d.ts +122 -0
  73. package/lib/settings.js +333 -0
  74. package/lib/translate/antigravity-schema.d.ts +24 -0
  75. package/lib/translate/antigravity-schema.js +174 -0
  76. package/lib/translate/antigravity.d.ts +184 -0
  77. package/lib/translate/antigravity.js +419 -0
  78. package/lib/translate/responses.d.ts +156 -0
  79. package/lib/translate/responses.js +395 -0
  80. package/lib/translate/sse.d.ts +27 -0
  81. package/lib/translate/sse.js +82 -0
  82. package/package.json +126 -4
@@ -0,0 +1,156 @@
1
+ /**
2
+ * The OpenAI Responses wire, as Kimi Code's `/coding/v1/responses` endpoint
3
+ * speaks it.
4
+ *
5
+ * Kimi's Responses route keeps a subscription account's thinking encrypted on
6
+ * the server: the stream still reports reasoning deltas, but a continuation
7
+ * replays the provider's own reasoning items rather than reconstructed text.
8
+ * That is why a stored `reasoning` block is never sent back as `reasoning_text`
9
+ * — the item shape the endpoint requires is provider-owned, so this plugin
10
+ * simply omits reasoning from `input` and lets each turn stand on its own
11
+ * visible content.
12
+ *
13
+ * `tool` results are represented as `function_call_output` items, and the
14
+ * request keeps them adjacent to their `function_call`, which this endpoint
15
+ * requires.
16
+ *
17
+ * @module dsh-plugin-subscription-oauth/translate/responses
18
+ */
19
+ import { LlmError } from '@deepseek-ai/dsh-llm';
20
+ import type { ContentBlock, RequestMessage, StreamChunk, TokenUsage, ToolSchema } from '@deepseek-ai/dsh-llm';
21
+ /** One image occurrence inlined as a data URL. */
22
+ export interface ResponsesImagePart {
23
+ /** MIME type verified by the attachment service. */
24
+ mediaType: string;
25
+ /** Base64-encoded bytes. */
26
+ base64: string;
27
+ }
28
+ /** Resolve one image block's bytes, or undefined when they are unreachable. */
29
+ export type ImageResolver = (ref: Extract<ContentBlock, {
30
+ type: 'image';
31
+ }>['attachment']) => Promise<ResponsesImagePart | undefined>;
32
+ /** Assembled `instructions` + `input` pair for one Responses request. */
33
+ export interface ResponsesRequestInput {
34
+ /** System text for the top-level `instructions` field; absent when there is none. */
35
+ instructions?: string;
36
+ /** Responses `input` items in conversation order. */
37
+ input: Record<string, unknown>[];
38
+ }
39
+ /**
40
+ * Convert harness messages into Responses `instructions` + `input` items.
41
+ *
42
+ * @param messages - the assembled request messages.
43
+ * @param system - explicit system prompt, which takes precedence over in-band ones.
44
+ * @param resolveImage - resolves an image block's bytes; an unresolved occurrence
45
+ * becomes a stable placeholder so the request still dispatches.
46
+ * @returns the request fields to merge into the body.
47
+ */
48
+ export declare function toResponsesInput(messages: readonly RequestMessage[], system?: string, resolveImage?: ImageResolver): Promise<ResponsesRequestInput>;
49
+ /**
50
+ * Map harness tool schemas onto Responses function tools.
51
+ *
52
+ * `strict` is set false to preserve the harness's optional parameters rather
53
+ * than letting the endpoint normalize every schema into strict mode.
54
+ *
55
+ * @param tools - the request's tool schemas.
56
+ * @returns `tools` array entries.
57
+ */
58
+ export declare function toResponsesTools(tools: readonly ToolSchema[]): Record<string, unknown>[];
59
+ /** The `usage` object of a completed response. */
60
+ export interface ResponsesUsage {
61
+ input_tokens?: number;
62
+ output_tokens?: number;
63
+ total_tokens?: number;
64
+ input_tokens_details?: {
65
+ cached_tokens?: number;
66
+ };
67
+ output_tokens_details?: {
68
+ reasoning_tokens?: number;
69
+ };
70
+ }
71
+ /**
72
+ * Map Responses usage onto the harness's disjoint counters.
73
+ * @param usage - wire usage from `response.completed`.
74
+ * @returns harness token usage.
75
+ */
76
+ export declare function mapResponsesUsage(usage: ResponsesUsage): TokenUsage;
77
+ /**
78
+ * Classify a Responses failure payload into a thrown error.
79
+ * @param code - provider error code, when present.
80
+ * @param message - provider error message, when present.
81
+ * @returns the mapped error (context overflow, quota, otherwise SERVER).
82
+ */
83
+ export declare function responsesFailure(code: string | undefined, message: string | undefined): LlmError;
84
+ /** The subset of Responses SSE event shapes this translator reads. */
85
+ export interface ResponsesStreamEvent {
86
+ type: string;
87
+ item_id?: string;
88
+ output_index?: number;
89
+ content_index?: number;
90
+ summary_index?: number;
91
+ delta?: string;
92
+ item?: {
93
+ type?: string;
94
+ id?: string;
95
+ call_id?: string;
96
+ name?: string;
97
+ arguments?: string;
98
+ encrypted_content?: string;
99
+ summary?: unknown[];
100
+ status?: string;
101
+ content?: {
102
+ type?: string;
103
+ text?: string;
104
+ }[];
105
+ };
106
+ response?: {
107
+ status?: string;
108
+ usage?: ResponsesUsage;
109
+ error?: {
110
+ code?: string;
111
+ message?: string;
112
+ };
113
+ incomplete_details?: {
114
+ reason?: string;
115
+ };
116
+ };
117
+ code?: string;
118
+ message?: string;
119
+ }
120
+ /**
121
+ * Push-model Responses translator: feed each parsed event to {@link push} and
122
+ * yield the chunks it returns.
123
+ *
124
+ * Block indexes are allocated in first-seen order; `usage` precedes the
125
+ * terminal `finish` and nothing is emitted after it; a terminal provider
126
+ * failure throws.
127
+ */
128
+ export declare class ResponsesStreamTranslator {
129
+ private readonly blocks;
130
+ private readonly order;
131
+ private nextIndex;
132
+ private sawToolCall;
133
+ /** Set once `response.completed` produced the terminal finish chunk. */
134
+ terminated: boolean;
135
+ private open;
136
+ private close;
137
+ /** Close every open block whose key belongs to one output item. */
138
+ private closeItem;
139
+ private closeAll;
140
+ /**
141
+ * Process one parsed Responses event.
142
+ * @param event - the parsed event object.
143
+ * @returns the chunks this event produced, possibly none.
144
+ * @throws {LlmError} on a terminal provider failure event.
145
+ */
146
+ push(event: ResponsesStreamEvent): StreamChunk[];
147
+ }
148
+ /**
149
+ * Consume a Responses SSE body and yield harness chunks.
150
+ * @param stream - the raw response body.
151
+ * @param onActivity - transport-activity callback for the idle watchdog.
152
+ * @yields harness stream chunks in contract order.
153
+ * @throws {LlmError} `MALFORMED_RESPONSE` on an undecodable event, `STREAM_CLOSED` when the
154
+ * body ends before `response.completed`.
155
+ */
156
+ export declare function streamResponses(stream: ReadableStream<Uint8Array>, onActivity?: () => void): AsyncGenerator<StreamChunk>;
@@ -0,0 +1,395 @@
1
+ /**
2
+ * The OpenAI Responses wire, as Kimi Code's `/coding/v1/responses` endpoint
3
+ * speaks it.
4
+ *
5
+ * Kimi's Responses route keeps a subscription account's thinking encrypted on
6
+ * the server: the stream still reports reasoning deltas, but a continuation
7
+ * replays the provider's own reasoning items rather than reconstructed text.
8
+ * That is why a stored `reasoning` block is never sent back as `reasoning_text`
9
+ * — the item shape the endpoint requires is provider-owned, so this plugin
10
+ * simply omits reasoning from `input` and lets each turn stand on its own
11
+ * visible content.
12
+ *
13
+ * `tool` results are represented as `function_call_output` items, and the
14
+ * request keeps them adjacent to their `function_call`, which this endpoint
15
+ * requires.
16
+ *
17
+ * @module dsh-plugin-subscription-oauth/translate/responses
18
+ */
19
+ import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, isContextWindowExceededError, isQuotaExceededError, LlmError, QUOTA_EXCEEDED_CODE, ToolCallId, } from '@deepseek-ai/dsh-llm';
20
+ import { parseSse } from './sse.js';
21
+ /** Read the text of one content block, when it has any. */
22
+ function blockText(block) {
23
+ switch (block.type) {
24
+ case 'text':
25
+ return block.text;
26
+ default:
27
+ return '';
28
+ }
29
+ }
30
+ /**
31
+ * Convert harness messages into Responses `instructions` + `input` items.
32
+ *
33
+ * @param messages - the assembled request messages.
34
+ * @param system - explicit system prompt, which takes precedence over in-band ones.
35
+ * @param resolveImage - resolves an image block's bytes; an unresolved occurrence
36
+ * becomes a stable placeholder so the request still dispatches.
37
+ * @returns the request fields to merge into the body.
38
+ */
39
+ export async function toResponsesInput(messages, system, resolveImage) {
40
+ const input = [];
41
+ const systemTexts = [];
42
+ if (system !== undefined && system.length > 0)
43
+ systemTexts.push(system);
44
+ for (const message of messages) {
45
+ switch (message.role) {
46
+ case 'system': {
47
+ const text = message.content.map(blockText).join('\n').trim();
48
+ if (text.length > 0)
49
+ systemTexts.push(text);
50
+ break;
51
+ }
52
+ case 'developer':
53
+ // Tool additions/removals have no Responses equivalent; declarations
54
+ // travel in the request's `tools`.
55
+ break;
56
+ case 'tool': {
57
+ const text = message.content.map(blockText).join('');
58
+ input.push({
59
+ type: 'function_call_output',
60
+ call_id: String(message.toolCallId),
61
+ output: message.isError === true && text.length === 0 ? 'tool call rejected' : text,
62
+ });
63
+ break;
64
+ }
65
+ case 'user': {
66
+ const content = [];
67
+ for (const block of message.content) {
68
+ if (block.type === 'text') {
69
+ if (block.text.length > 0)
70
+ content.push({ type: 'input_text', text: block.text });
71
+ }
72
+ else if (block.type === 'image') {
73
+ const resolved = resolveImage === undefined ? undefined : await resolveImage(block.attachment);
74
+ content.push(resolved === undefined
75
+ ? { type: 'input_text', text: '[image unavailable]' }
76
+ : { type: 'input_image', image_url: `data:${resolved.mediaType};base64,${resolved.base64}` });
77
+ }
78
+ else if (block.type === 'file') {
79
+ content.push({ type: 'input_text', text: block.attachment.name ?? '[file]' });
80
+ }
81
+ }
82
+ if (content.length > 0)
83
+ input.push({ type: 'message', role: 'user', content });
84
+ break;
85
+ }
86
+ case 'assistant': {
87
+ const content = [];
88
+ const flush = () => {
89
+ if (content.length === 0)
90
+ return;
91
+ input.push({ type: 'message', role: 'assistant', content: [...content] });
92
+ content.length = 0;
93
+ };
94
+ for (const block of message.content) {
95
+ if (block.type === 'text') {
96
+ if (block.text.length > 0)
97
+ content.push({ type: 'output_text', text: block.text });
98
+ }
99
+ else if (block.type === 'tool-call') {
100
+ flush();
101
+ input.push({
102
+ type: 'function_call',
103
+ call_id: String(block.id),
104
+ name: block.name,
105
+ arguments: block.arguments,
106
+ });
107
+ }
108
+ // reasoning: never replayed; see the module doc.
109
+ }
110
+ flush();
111
+ break;
112
+ }
113
+ default:
114
+ break;
115
+ }
116
+ }
117
+ const instructions = systemTexts.length > 0 ? systemTexts.join('\n\n') : undefined;
118
+ return { ...instructions === undefined ? {} : { instructions }, input };
119
+ }
120
+ /**
121
+ * Map harness tool schemas onto Responses function tools.
122
+ *
123
+ * `strict` is set false to preserve the harness's optional parameters rather
124
+ * than letting the endpoint normalize every schema into strict mode.
125
+ *
126
+ * @param tools - the request's tool schemas.
127
+ * @returns `tools` array entries.
128
+ */
129
+ export function toResponsesTools(tools) {
130
+ return tools.map((tool) => ({
131
+ type: 'function',
132
+ name: tool.name,
133
+ description: tool.description,
134
+ parameters: tool.parameters,
135
+ strict: false,
136
+ }));
137
+ }
138
+ /**
139
+ * Map Responses usage onto the harness's disjoint counters.
140
+ * @param usage - wire usage from `response.completed`.
141
+ * @returns harness token usage.
142
+ */
143
+ export function mapResponsesUsage(usage) {
144
+ const cached = usage.input_tokens_details?.cached_tokens;
145
+ const reasoning = usage.output_tokens_details?.reasoning_tokens;
146
+ const prompt = usage.input_tokens ?? 0;
147
+ return {
148
+ inputTokens: Math.max(0, prompt - (cached ?? 0)),
149
+ outputTokens: usage.output_tokens ?? 0,
150
+ ...usage.total_tokens === undefined ? {} : { totalTokens: usage.total_tokens },
151
+ ...cached === undefined ? {} : { cacheReadTokens: cached },
152
+ ...reasoning === undefined ? {} : { reasoningTokens: reasoning },
153
+ };
154
+ }
155
+ /**
156
+ * Classify a Responses failure payload into a thrown error.
157
+ * @param code - provider error code, when present.
158
+ * @param message - provider error message, when present.
159
+ * @returns the mapped error (context overflow, quota, otherwise SERVER).
160
+ */
161
+ export function responsesFailure(code, message) {
162
+ const text = message ?? code ?? 'the provider reported a failed response';
163
+ const detail = `${code ?? ''} ${message ?? ''}`;
164
+ if (code === 'context_window_exceeded' || isContextWindowExceededError(detail)) {
165
+ return new LlmError(text, CONTEXT_WINDOW_EXCEEDED_CODE);
166
+ }
167
+ if ((code !== undefined && /insufficient|quota/i.test(code)) || isQuotaExceededError(detail)) {
168
+ return new LlmError(text, QUOTA_EXCEEDED_CODE);
169
+ }
170
+ return new LlmError(text, 'SERVER');
171
+ }
172
+ /** Assemble the final content block for one closed block. */
173
+ function closeBlock(block) {
174
+ switch (block.kind) {
175
+ case 'text':
176
+ return { type: 'text', text: block.text };
177
+ case 'reasoning':
178
+ return { type: 'reasoning', text: block.text };
179
+ case 'tool-call':
180
+ return {
181
+ type: 'tool-call',
182
+ id: ToolCallId(block.callId),
183
+ name: block.name ?? '',
184
+ arguments: block.text,
185
+ };
186
+ }
187
+ }
188
+ /**
189
+ * Push-model Responses translator: feed each parsed event to {@link push} and
190
+ * yield the chunks it returns.
191
+ *
192
+ * Block indexes are allocated in first-seen order; `usage` precedes the
193
+ * terminal `finish` and nothing is emitted after it; a terminal provider
194
+ * failure throws.
195
+ */
196
+ export class ResponsesStreamTranslator {
197
+ blocks = new Map();
198
+ order = [];
199
+ nextIndex = 0;
200
+ sawToolCall = false;
201
+ /** Set once `response.completed` produced the terminal finish chunk. */
202
+ terminated = false;
203
+ open(key, kind, chunks, callId = '', name) {
204
+ const block = {
205
+ index: this.nextIndex++,
206
+ kind,
207
+ text: '',
208
+ callId,
209
+ ...name === undefined ? {} : { name },
210
+ };
211
+ this.blocks.set(key, block);
212
+ this.order.push(block);
213
+ chunks.push({ type: 'block-start', index: block.index, blockType: kind });
214
+ return block;
215
+ }
216
+ close(key, chunks) {
217
+ const block = this.blocks.get(key);
218
+ if (block === undefined)
219
+ return;
220
+ this.blocks.delete(key);
221
+ chunks.push({ type: 'block-end', index: block.index, block: closeBlock(block) });
222
+ }
223
+ /** Close every open block whose key belongs to one output item. */
224
+ closeItem(itemId, chunks) {
225
+ for (const key of [...this.blocks.keys()]) {
226
+ if (key.startsWith(`${itemId}:`))
227
+ this.close(key, chunks);
228
+ }
229
+ }
230
+ closeAll(chunks) {
231
+ for (const key of [...this.blocks.keys()])
232
+ this.close(key, chunks);
233
+ }
234
+ /**
235
+ * Process one parsed Responses event.
236
+ * @param event - the parsed event object.
237
+ * @returns the chunks this event produced, possibly none.
238
+ * @throws {LlmError} on a terminal provider failure event.
239
+ */
240
+ push(event) {
241
+ if (this.terminated)
242
+ return [];
243
+ const chunks = [];
244
+ switch (event.type) {
245
+ case 'response.output_item.added': {
246
+ const item = event.item;
247
+ if (item?.type === 'function_call' && item.id !== undefined) {
248
+ this.sawToolCall = true;
249
+ const callId = item.call_id ?? '';
250
+ const block = this.open(`${item.id}:call`, 'tool-call', chunks, callId, item.name);
251
+ chunks.push({
252
+ type: 'tool-call-delta',
253
+ index: block.index,
254
+ id: ToolCallId(callId),
255
+ ...item.name === undefined ? {} : { name: item.name },
256
+ argumentsDelta: '',
257
+ });
258
+ }
259
+ return chunks;
260
+ }
261
+ case 'response.output_text.delta': {
262
+ const key = `${event.item_id ?? ''}:text:${String(event.content_index ?? 0)}`;
263
+ const block = this.blocks.get(key) ?? this.open(key, 'text', chunks);
264
+ const text = event.delta ?? '';
265
+ block.text += text;
266
+ chunks.push({ type: 'text-delta', index: block.index, text });
267
+ return chunks;
268
+ }
269
+ case 'response.reasoning_summary_text.delta':
270
+ case 'response.reasoning_text.delta': {
271
+ const part = event.summary_index ?? event.content_index ?? 0;
272
+ const key = `${event.item_id ?? ''}:reason:${String(part)}`;
273
+ const block = this.blocks.get(key) ?? this.open(key, 'reasoning', chunks);
274
+ const text = event.delta ?? '';
275
+ block.text += text;
276
+ chunks.push({ type: 'reasoning-delta', index: block.index, text });
277
+ return chunks;
278
+ }
279
+ case 'response.function_call_arguments.delta': {
280
+ const key = `${event.item_id ?? ''}:call`;
281
+ let block = this.blocks.get(key);
282
+ if (block === undefined) {
283
+ // The item.added event was missed; open the block from the delta alone.
284
+ this.sawToolCall = true;
285
+ block = this.open(key, 'tool-call', chunks);
286
+ }
287
+ const text = event.delta ?? '';
288
+ block.text += text;
289
+ chunks.push({
290
+ type: 'tool-call-delta',
291
+ index: block.index,
292
+ id: ToolCallId(block.callId),
293
+ ...block.name === undefined ? {} : { name: block.name },
294
+ argumentsDelta: text,
295
+ });
296
+ return chunks;
297
+ }
298
+ case 'response.output_item.done': {
299
+ const item = event.item;
300
+ if (item === undefined || item.id === undefined) {
301
+ this.closeAll(chunks);
302
+ return chunks;
303
+ }
304
+ if (item.type === 'function_call') {
305
+ const key = `${item.id}:call`;
306
+ let block = this.blocks.get(key);
307
+ if (block === undefined) {
308
+ this.sawToolCall = true;
309
+ block = this.open(key, 'tool-call', chunks, item.call_id ?? '', item.name);
310
+ }
311
+ // A complete item may deliver its arguments only on done.
312
+ if (block.text.length === 0 && item.arguments !== undefined)
313
+ block.text = item.arguments;
314
+ this.close(key, chunks);
315
+ }
316
+ else if (item.type === 'message') {
317
+ const hasDeltas = [...this.blocks.keys()].some((key) => key.startsWith(`${item.id}:text:`));
318
+ if (!hasDeltas) {
319
+ for (const [partIndex, part] of (item.content ?? []).entries()) {
320
+ if (part?.type !== 'output_text' || typeof part.text !== 'string' || part.text.length === 0)
321
+ continue;
322
+ const key = `${item.id}:text:${String(partIndex)}`;
323
+ const block = this.open(key, 'text', chunks);
324
+ block.text = part.text;
325
+ chunks.push({ type: 'text-delta', index: block.index, text: part.text });
326
+ this.close(key, chunks);
327
+ }
328
+ }
329
+ this.closeItem(item.id, chunks);
330
+ }
331
+ else {
332
+ this.closeItem(item.id, chunks);
333
+ }
334
+ return chunks;
335
+ }
336
+ case 'response.completed': {
337
+ this.terminated = true;
338
+ this.closeAll(chunks);
339
+ const usage = event.response?.usage;
340
+ if (usage !== undefined)
341
+ chunks.push({ type: 'usage', usage: mapResponsesUsage(usage) });
342
+ if (this.order.length === 0) {
343
+ chunks.push({
344
+ type: 'finish',
345
+ reason: {
346
+ kind: 'error',
347
+ failure: {
348
+ message: 'model returned a completed response with no content',
349
+ code: EMPTY_RESPONSE_CODE,
350
+ },
351
+ },
352
+ });
353
+ }
354
+ else {
355
+ chunks.push({ type: 'finish', reason: { kind: this.sawToolCall ? 'tool-calls' : 'stop' } });
356
+ }
357
+ return chunks;
358
+ }
359
+ case 'response.failed':
360
+ throw responsesFailure(event.response?.error?.code, event.response?.error?.message);
361
+ case 'response.incomplete':
362
+ throw responsesFailure(event.response?.incomplete_details?.reason, event.response?.error?.message
363
+ ?? `the provider reported an incomplete response (${event.response?.incomplete_details?.reason ?? 'unknown reason'})`);
364
+ case 'error':
365
+ throw responsesFailure(event.code, event.message);
366
+ default:
367
+ // response.created, response.in_progress, content_part events, etc.
368
+ return chunks;
369
+ }
370
+ }
371
+ }
372
+ /**
373
+ * Consume a Responses SSE body and yield harness chunks.
374
+ * @param stream - the raw response body.
375
+ * @param onActivity - transport-activity callback for the idle watchdog.
376
+ * @yields harness stream chunks in contract order.
377
+ * @throws {LlmError} `MALFORMED_RESPONSE` on an undecodable event, `STREAM_CLOSED` when the
378
+ * body ends before `response.completed`.
379
+ */
380
+ export async function* streamResponses(stream, onActivity) {
381
+ const translator = new ResponsesStreamTranslator();
382
+ for await (const event of parseSse(stream, onActivity)) {
383
+ let decoded;
384
+ try {
385
+ decoded = JSON.parse(event.data);
386
+ }
387
+ catch {
388
+ throw new LlmError(`malformed SSE payload: ${event.data.slice(0, 120)}`, 'MALFORMED_RESPONSE');
389
+ }
390
+ yield* translator.push(decoded);
391
+ if (translator.terminated)
392
+ return;
393
+ }
394
+ throw new LlmError('Kimi Responses stream ended before response.completed', 'STREAM_CLOSED');
395
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Server-sent-events reader for one provider response body.
3
+ *
4
+ * The reader is deliberately protocol-only: it reassembles transport chunks
5
+ * into whole events and leaves interpretation to the wire layer above it.
6
+ *
7
+ * @module dsh-plugin-subscription-oauth/translate/sse
8
+ */
9
+ /** One decoded SSE event. */
10
+ export interface SseEvent {
11
+ /** Joined `data:` lines of the event. */
12
+ readonly data: string;
13
+ /** `event:` field, when the server set one. */
14
+ readonly event?: string;
15
+ }
16
+ /**
17
+ * Decode an SSE byte stream into events.
18
+ *
19
+ * Multiple `data:` lines of one event are joined with `\n`, per the SSE
20
+ * specification, and `:` comment lines (often keep-alive pings) are skipped
21
+ * but still reported as transport activity so an idle watchdog can see them.
22
+ *
23
+ * @param stream - the raw response body.
24
+ * @param onActivity - called for every decoded line, including comments.
25
+ * @yields each complete event in arrival order.
26
+ */
27
+ export declare function parseSse(stream: ReadableStream<Uint8Array>, onActivity?: () => void): AsyncGenerator<SseEvent>;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Server-sent-events reader for one provider response body.
3
+ *
4
+ * The reader is deliberately protocol-only: it reassembles transport chunks
5
+ * into whole events and leaves interpretation to the wire layer above it.
6
+ *
7
+ * @module dsh-plugin-subscription-oauth/translate/sse
8
+ */
9
+ /** Apply one already-unfolded SSE line to the pending event. */
10
+ function applyLine(pending, line) {
11
+ const colon = line.indexOf(':');
12
+ const field = colon < 0 ? line : line.slice(0, colon);
13
+ let value = colon < 0 ? '' : line.slice(colon + 1);
14
+ if (value.startsWith(' '))
15
+ value = value.slice(1);
16
+ if (field === 'data')
17
+ pending.data.push(value);
18
+ else if (field === 'event')
19
+ pending.event = value;
20
+ }
21
+ /** Convert a pending event into a yielded value, or undefined when empty. */
22
+ function asEvent(pending) {
23
+ if (pending.data.length === 0 && pending.event === undefined)
24
+ return undefined;
25
+ const data = pending.data.join('\n');
26
+ return pending.event === undefined ? { data } : { data, event: pending.event };
27
+ }
28
+ /**
29
+ * Decode an SSE byte stream into events.
30
+ *
31
+ * Multiple `data:` lines of one event are joined with `\n`, per the SSE
32
+ * specification, and `:` comment lines (often keep-alive pings) are skipped
33
+ * but still reported as transport activity so an idle watchdog can see them.
34
+ *
35
+ * @param stream - the raw response body.
36
+ * @param onActivity - called for every decoded line, including comments.
37
+ * @yields each complete event in arrival order.
38
+ */
39
+ export async function* parseSse(stream, onActivity) {
40
+ const reader = stream.getReader();
41
+ const decoder = new TextDecoder();
42
+ let buffer = '';
43
+ let pending = { data: [] };
44
+ try {
45
+ for (;;) {
46
+ const { done, value } = await reader.read();
47
+ if (done)
48
+ break;
49
+ buffer += decoder.decode(value, { stream: true });
50
+ let newline = buffer.indexOf('\n');
51
+ while (newline >= 0) {
52
+ const line = buffer.slice(0, newline).replace(/\r$/, '');
53
+ buffer = buffer.slice(newline + 1);
54
+ if (line.length === 0) {
55
+ const event = asEvent(pending);
56
+ if (event !== undefined)
57
+ yield event;
58
+ pending = { data: [] };
59
+ }
60
+ else {
61
+ if (line.startsWith(':'))
62
+ onActivity?.();
63
+ else
64
+ applyLine(pending, line);
65
+ onActivity?.();
66
+ }
67
+ newline = buffer.indexOf('\n');
68
+ }
69
+ }
70
+ // A stream that ends without a terminating blank line still carries a
71
+ // usable final event; flush it rather than dropping the payload.
72
+ const tail = buffer.replace(/\r$/, '');
73
+ if (tail.length > 0)
74
+ applyLine(pending, tail);
75
+ const last = asEvent(pending);
76
+ if (last !== undefined)
77
+ yield last;
78
+ }
79
+ finally {
80
+ await reader.cancel().catch(() => undefined);
81
+ }
82
+ }