dsh-subscriptions 0.1.3 → 0.1.5
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 +64 -0
- package/lib/auth/device-flow.js +6 -3
- package/lib/auth/oauth-flow.js +41 -8
- package/lib/auth/rpc.d.ts +3 -1
- package/lib/auth/rpc.js +10 -1
- package/lib/client/ProviderAccountManager.js +34 -41
- package/lib/client/ProviderModelEditor.js +5 -9
- package/lib/client/SpeedSelect.js +8 -7
- package/lib/client/SubscriptionUsageBadge.js +6 -5
- package/lib/client/SubscriptionsSection.d.ts +21 -73
- package/lib/client/SubscriptionsSection.js +130 -104
- package/lib/client/UsageBadgeDisplaySetting.js +4 -8
- package/lib/client/host-contract.d.ts +1 -1
- package/lib/client/host-contract.js +22 -0
- package/lib/client/locales.d.ts +58 -0
- package/lib/client/locales.js +58 -0
- package/lib/client/provider-settings-styles.d.ts +3 -0
- package/lib/client/provider-settings-styles.js +206 -0
- package/lib/client/reset-action.d.ts +14 -0
- package/lib/client/reset-action.js +15 -0
- package/lib/client/reset-credits-view.d.ts +53 -0
- package/lib/client/reset-credits-view.js +103 -0
- package/lib/client.js +937 -310
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +9 -0
- package/lib/index.js +6058 -5298
- package/lib/model-defaults.d.ts +4 -5
- package/lib/model-defaults.js +18 -14
- package/lib/providers/account-preferences.js +3 -2
- package/lib/providers/antigravity.d.ts +0 -2
- package/lib/providers/antigravity.js +5 -9
- package/lib/providers/catalog-store.d.ts +3 -2
- package/lib/providers/catalog-store.js +57 -26
- package/lib/providers/claude.d.ts +22 -3
- package/lib/providers/claude.js +118 -20
- package/lib/providers/codex-search.js +5 -2
- package/lib/providers/codex.d.ts +15 -2
- package/lib/providers/codex.js +77 -9
- package/lib/providers/common.d.ts +38 -0
- package/lib/providers/common.js +44 -18
- package/lib/providers/copilot.d.ts +2 -1
- package/lib/providers/copilot.js +13 -6
- package/lib/providers/cursor-adapter.d.ts +35 -0
- package/lib/providers/cursor-adapter.js +245 -4
- package/lib/providers/cursor-auth.js +11 -4
- package/lib/providers/cursor-usage.js +2 -1
- package/lib/providers/external-usage.js +2 -1
- package/lib/providers/grok.d.ts +2 -2
- package/lib/providers/grok.js +16 -11
- package/lib/providers/minimax-usage.js +2 -1
- package/lib/providers/pool-health.d.ts +2 -0
- package/lib/providers/pool-health.js +9 -0
- package/lib/providers/pool-usage.d.ts +13 -1
- package/lib/providers/pool-usage.js +39 -7
- package/lib/providers/pool.d.ts +7 -5
- package/lib/providers/pool.js +89 -12
- package/lib/providers/rate-limit.d.ts +0 -14
- package/lib/providers/rate-limit.js +0 -26
- package/lib/providers/replay.d.ts +4 -0
- package/lib/providers/replay.js +50 -0
- package/lib/providers/reset-redemption.d.ts +51 -0
- package/lib/providers/reset-redemption.js +102 -0
- package/lib/tools/image-generate.js +2 -1
- package/lib/tools/video-generate.d.ts +0 -5
- package/lib/tools/video-generate.js +25 -14
- package/lib/tools/x-search.js +2 -2
- package/lib/translate/anthropic.d.ts +21 -2
- package/lib/translate/anthropic.js +35 -11
- package/lib/translate/antigravity.js +13 -6
- package/lib/translate/chat-completions.js +1 -1
- package/lib/translate/resolved.d.ts +24 -0
- package/lib/translate/resolved.js +66 -3
- package/lib/translate/responses.js +8 -6
- package/lib/translate/sse.js +8 -0
- package/package.json +1 -1
|
@@ -32,6 +32,19 @@ export const CACHE_BLOCK_STRIDE = 15;
|
|
|
32
32
|
* blocks before a read is lost.
|
|
33
33
|
*/
|
|
34
34
|
export const MESSAGE_CACHE_BREAKPOINTS = 3;
|
|
35
|
+
/** The API default; a marker at this TTL carries no `ttl` field at all. */
|
|
36
|
+
export const DEFAULT_PROMPT_CACHE_TTL = '5m';
|
|
37
|
+
/**
|
|
38
|
+
* Build one `cache_control` marker.
|
|
39
|
+
*
|
|
40
|
+
* Five minutes is spelled by omitting `ttl`, so a request that does not opt in
|
|
41
|
+
* stays byte-identical to what earlier releases sent.
|
|
42
|
+
* @param ttl - cache lifetime; defaults to five minutes.
|
|
43
|
+
* @returns a fresh marker object (callers attach it to one block each).
|
|
44
|
+
*/
|
|
45
|
+
export function cacheControl(ttl = DEFAULT_PROMPT_CACHE_TTL) {
|
|
46
|
+
return ttl === '1h' ? { type: 'ephemeral', ttl: '1h' } : { type: 'ephemeral' };
|
|
47
|
+
}
|
|
35
48
|
/** Preserve native image blocks, retaining the existing text-only wire shape. */
|
|
36
49
|
function toolResultContent(block) {
|
|
37
50
|
if (!block.content.some(part => part.type === 'image' && 'dataBase64' in part)) {
|
|
@@ -217,14 +230,15 @@ export function toAnthropicMessages(messages) {
|
|
|
217
230
|
* Marks are counted across the flattened block sequence, not per message,
|
|
218
231
|
* because the lookback window Anthropic walks counts blocks the same way.
|
|
219
232
|
* @param messages - assembled Anthropic messages, marked in place.
|
|
233
|
+
* @param ttl - cache lifetime for every mark; defaults to five minutes.
|
|
220
234
|
*/
|
|
221
|
-
export function markMessageCache(messages) {
|
|
235
|
+
export function markMessageCache(messages, ttl) {
|
|
222
236
|
const blocks = messages.flatMap(message => message.content);
|
|
223
237
|
for (let mark = 0; mark < MESSAGE_CACHE_BREAKPOINTS; mark++) {
|
|
224
238
|
const at = blocks.length - 1 - mark * CACHE_BLOCK_STRIDE;
|
|
225
239
|
if (at < 0)
|
|
226
240
|
return;
|
|
227
|
-
blocks[at].cache_control =
|
|
241
|
+
blocks[at].cache_control = cacheControl(ttl);
|
|
228
242
|
}
|
|
229
243
|
}
|
|
230
244
|
/**
|
|
@@ -233,9 +247,10 @@ export function markMessageCache(messages) {
|
|
|
233
247
|
* @param system - explicit system prompt, when set.
|
|
234
248
|
* @param messages - conversation messages; the system-role text preceding the
|
|
235
249
|
* conversation is appended, and a later one is left to {@link toAnthropicMessages}.
|
|
250
|
+
* @param ttl - cache lifetime for the tools+system mark; defaults to five minutes.
|
|
236
251
|
* @returns the system content blocks.
|
|
237
252
|
*/
|
|
238
|
-
export function toAnthropicSystem(system, messages) {
|
|
253
|
+
export function toAnthropicSystem(system, messages, ttl) {
|
|
239
254
|
const blocks = [{ type: 'text', text: CLAUDE_CODE_IDENTITY }];
|
|
240
255
|
if (system !== undefined && system.length > 0)
|
|
241
256
|
blocks.push({ type: 'text', text: system });
|
|
@@ -249,7 +264,11 @@ export function toAnthropicSystem(system, messages) {
|
|
|
249
264
|
// `tools` renders ahead of `system`, so this one marker caches both. It is
|
|
250
265
|
// deliberately separate from the message marks: a tool_choice or thinking
|
|
251
266
|
// change invalidates the messages tier only, and this entry survives it.
|
|
252
|
-
|
|
267
|
+
//
|
|
268
|
+
// Anthropic processes `tools`, `system`, then `messages`, and rejects a
|
|
269
|
+
// one-hour mark that follows a five-minute one (HTTP 400). This mark and the
|
|
270
|
+
// message marks therefore share one TTL, which can never hit that ordering.
|
|
271
|
+
blocks[blocks.length - 1].cache_control = cacheControl(ttl);
|
|
253
272
|
return blocks;
|
|
254
273
|
}
|
|
255
274
|
/**
|
|
@@ -294,16 +313,21 @@ function closeBlock(block) {
|
|
|
294
313
|
* @returns the mapped error.
|
|
295
314
|
*/
|
|
296
315
|
export function anthropicFailure(error) {
|
|
316
|
+
// Safe here means this diagnostic path carries no provider-controlled free text,
|
|
317
|
+
// not that no secret can exist anywhere. Raw values remain local classification inputs.
|
|
297
318
|
const type = error?.type ?? 'unknown_error';
|
|
298
319
|
const message = error?.message ?? `Anthropic reported ${type}`;
|
|
320
|
+
let classification = 'SERVER';
|
|
299
321
|
if (type === 'invalid_request_error' && /prompt is too long/i.test(message)) {
|
|
300
|
-
|
|
322
|
+
classification = CONTEXT_WINDOW_EXCEEDED_CODE;
|
|
323
|
+
}
|
|
324
|
+
else if (type === 'rate_limit_error') {
|
|
325
|
+
classification = 'RATE_LIMIT';
|
|
326
|
+
}
|
|
327
|
+
else if (type === 'authentication_error') {
|
|
328
|
+
classification = 'AUTH';
|
|
301
329
|
}
|
|
302
|
-
|
|
303
|
-
return new LlmError(message, 'RATE_LIMIT');
|
|
304
|
-
if (type === 'authentication_error')
|
|
305
|
-
return new LlmError(message, 'AUTH');
|
|
306
|
-
return new LlmError(message, 'SERVER');
|
|
330
|
+
return new LlmError(`Anthropic failure (${classification}): [provider response body omitted]`, classification);
|
|
307
331
|
}
|
|
308
332
|
/**
|
|
309
333
|
* Push-model Anthropic SSE translator: feed each parsed event object to
|
|
@@ -505,7 +529,7 @@ export async function* streamAnthropic(stream, onActivity) {
|
|
|
505
529
|
event = JSON.parse(sseEvent.data);
|
|
506
530
|
}
|
|
507
531
|
catch {
|
|
508
|
-
throw new LlmError(
|
|
532
|
+
throw new LlmError('malformed SSE payload (MALFORMED_RESPONSE): [provider response body omitted]', 'MALFORMED_RESPONSE');
|
|
509
533
|
}
|
|
510
534
|
yield* translator.push(event);
|
|
511
535
|
if (translator.terminated)
|
|
@@ -15,9 +15,11 @@ import { antigravityToolParameters } from './antigravity-schema.js';
|
|
|
15
15
|
* `FunctionResponse.response` field requires. That field maps to a singular
|
|
16
16
|
* protobuf `Struct`, so a bare array, string, number or boolean is rejected
|
|
17
17
|
* by the endpoint with "Proto field is not repeating, cannot start list".
|
|
18
|
-
*
|
|
18
|
+
* Non-array JSON objects use their own fields; every other JSON value is
|
|
19
19
|
* wrapped in the same "output" envelope the non-JSON path already uses --
|
|
20
20
|
* the shape other Cloud Code Assist clients send for every tool result.
|
|
21
|
+
* Failed results carry top-level `isError: true`, overriding a payload field;
|
|
22
|
+
* successful results retain their existing serialized shape.
|
|
21
23
|
*/
|
|
22
24
|
function toolResultValue(block) {
|
|
23
25
|
const text = block.content.map(part => part.type === 'text' ? part.text : '').join('');
|
|
@@ -28,9 +30,11 @@ function toolResultValue(block) {
|
|
|
28
30
|
catch {
|
|
29
31
|
return { output: text, ...block.isError === true ? { isError: true } : {} };
|
|
30
32
|
}
|
|
31
|
-
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
34
|
+
const value = parsed;
|
|
35
|
+
return block.isError === true ? { ...value, isError: true } : value;
|
|
36
|
+
}
|
|
37
|
+
return { output: parsed, ...block.isError === true ? { isError: true } : {} };
|
|
34
38
|
}
|
|
35
39
|
/** Safely read per-block replay metadata emitted by this adapter. */
|
|
36
40
|
function replayBlocks(message, model) {
|
|
@@ -88,7 +92,10 @@ export function toAntigravityContents(messages, model) {
|
|
|
88
92
|
const part = { functionResponse: {
|
|
89
93
|
id,
|
|
90
94
|
name: callNames.get(id) ?? '',
|
|
91
|
-
response: toolResultValue({
|
|
95
|
+
response: toolResultValue({
|
|
96
|
+
type: 'tool-result', toolCallId: ToolCallId(id), content: message.content,
|
|
97
|
+
...message.isError === undefined ? {} : { isError: message.isError },
|
|
98
|
+
}),
|
|
92
99
|
} };
|
|
93
100
|
const previous = out.at(-1);
|
|
94
101
|
if (previous?.role === 'user')
|
|
@@ -350,7 +357,7 @@ export async function* streamAntigravity(stream, onActivity) {
|
|
|
350
357
|
parsed = JSON.parse(event.data);
|
|
351
358
|
}
|
|
352
359
|
catch {
|
|
353
|
-
throw new LlmError(
|
|
360
|
+
throw new LlmError('malformed Antigravity SSE payload (MALFORMED_RESPONSE): [provider response body omitted]', 'MALFORMED_RESPONSE');
|
|
354
361
|
}
|
|
355
362
|
yield* translator.push(parsed);
|
|
356
363
|
if (translator.terminated)
|
|
@@ -369,7 +369,7 @@ export async function* streamChatCompletions(stream, onActivity) {
|
|
|
369
369
|
event = JSON.parse(sseEvent.data);
|
|
370
370
|
}
|
|
371
371
|
catch {
|
|
372
|
-
throw new LlmError(
|
|
372
|
+
throw new LlmError('malformed SSE payload (MALFORMED_RESPONSE): [provider response body omitted]', 'MALFORMED_RESPONSE');
|
|
373
373
|
}
|
|
374
374
|
yield* translator.push(event);
|
|
375
375
|
if (translator.terminated)
|
|
@@ -50,6 +50,30 @@ export interface ImageRequestLimit {
|
|
|
50
50
|
/** Encoded-byte target before base64 expansion. */
|
|
51
51
|
maxBytes: number;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Failure code a host's image-offload executor answers by replacing the oldest
|
|
55
|
+
* retained images with text and retrying. Every host in our support window
|
|
56
|
+
* exports the equivalent as `IMAGE_OFFLOAD_REQUIRED_CODE`, from the window floor
|
|
57
|
+
* `0.1.7-rc.2` up, so this is not a newer-host path. The value is spelled out
|
|
58
|
+
* rather than imported so the plugin keeps building if a host stops exporting
|
|
59
|
+
* it.
|
|
60
|
+
*/
|
|
61
|
+
export declare const IMAGE_OFFLOAD_REQUIRED = "IMAGE_OFFLOAD_REQUIRED";
|
|
62
|
+
/**
|
|
63
|
+
* Whether the installed host records image offloads. Hosts that predate the
|
|
64
|
+
* executor never mark images `offloaded`, so asking them to would fail every
|
|
65
|
+
* later request instead of shrinking it.
|
|
66
|
+
*/
|
|
67
|
+
export declare function hostSupportsImageOffload(): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* How many of the oldest images still sent inline must be offloaded before
|
|
70
|
+
* the resolved request fits `maxBase64Bytes` of image data; zero when it fits.
|
|
71
|
+
* Images are counted depth-first in request order, skipping assistant
|
|
72
|
+
* messages, the same order the host's executor selects occurrences in.
|
|
73
|
+
* @param messages - the resolved request, after {@link resolveImages}.
|
|
74
|
+
* @param maxBase64Bytes - the route's budget for all inline image data.
|
|
75
|
+
*/
|
|
76
|
+
export declare function requiredImageOffloadCount(messages: readonly TranslatableMessage[], maxBase64Bytes: number): number;
|
|
53
77
|
/**
|
|
54
78
|
* The request projection for one oversized image, or undefined when it fits.
|
|
55
79
|
* Hosts before DSH 0.1.7 read a pixel budget (`maxPixels`), later ones the
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* base64 data.
|
|
7
7
|
*/
|
|
8
8
|
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
9
|
+
import * as llm from '@deepseek-ai/dsh-llm';
|
|
9
10
|
/**
|
|
10
11
|
* Wires with text-only tool outputs receive images in a following user turn.
|
|
11
12
|
* Defer that turn until all consecutive user messages have been processed:
|
|
@@ -42,6 +43,55 @@ export function withToolResultImages(messages) {
|
|
|
42
43
|
flush();
|
|
43
44
|
return out;
|
|
44
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Failure code a host's image-offload executor answers by replacing the oldest
|
|
48
|
+
* retained images with text and retrying. Every host in our support window
|
|
49
|
+
* exports the equivalent as `IMAGE_OFFLOAD_REQUIRED_CODE`, from the window floor
|
|
50
|
+
* `0.1.7-rc.2` up, so this is not a newer-host path. The value is spelled out
|
|
51
|
+
* rather than imported so the plugin keeps building if a host stops exporting
|
|
52
|
+
* it.
|
|
53
|
+
*/
|
|
54
|
+
export const IMAGE_OFFLOAD_REQUIRED = 'IMAGE_OFFLOAD_REQUIRED';
|
|
55
|
+
/**
|
|
56
|
+
* Whether the installed host records image offloads. Hosts that predate the
|
|
57
|
+
* executor never mark images `offloaded`, so asking them to would fail every
|
|
58
|
+
* later request instead of shrinking it.
|
|
59
|
+
*/
|
|
60
|
+
export function hostSupportsImageOffload() {
|
|
61
|
+
const exports = llm;
|
|
62
|
+
return exports['IMAGE_OFFLOAD_REQUIRED_CODE'] === IMAGE_OFFLOAD_REQUIRED
|
|
63
|
+
&& typeof exports['offloadedImageText'] === 'function';
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* How many of the oldest images still sent inline must be offloaded before
|
|
67
|
+
* the resolved request fits `maxBase64Bytes` of image data; zero when it fits.
|
|
68
|
+
* Images are counted depth-first in request order, skipping assistant
|
|
69
|
+
* messages, the same order the host's executor selects occurrences in.
|
|
70
|
+
* @param messages - the resolved request, after {@link resolveImages}.
|
|
71
|
+
* @param maxBase64Bytes - the route's budget for all inline image data.
|
|
72
|
+
*/
|
|
73
|
+
export function requiredImageOffloadCount(messages, maxBase64Bytes) {
|
|
74
|
+
const lengths = [];
|
|
75
|
+
const visit = (blocks) => {
|
|
76
|
+
for (const block of blocks) {
|
|
77
|
+
if (block.type === 'image' && 'dataBase64' in block)
|
|
78
|
+
lengths.push(block.dataBase64.length);
|
|
79
|
+
else if (block.type === 'tool-result')
|
|
80
|
+
visit(block.content);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
for (const message of messages) {
|
|
84
|
+
if (message.role !== 'assistant')
|
|
85
|
+
visit(message.content);
|
|
86
|
+
}
|
|
87
|
+
let excess = lengths.reduce((sum, bytes) => sum + bytes, 0) - maxBase64Bytes;
|
|
88
|
+
let count = 0;
|
|
89
|
+
while (excess > 0 && count < lengths.length) {
|
|
90
|
+
excess -= lengths[count];
|
|
91
|
+
count += 1;
|
|
92
|
+
}
|
|
93
|
+
return count;
|
|
94
|
+
}
|
|
45
95
|
/**
|
|
46
96
|
* The request projection for one oversized image, or undefined when it fits.
|
|
47
97
|
* Hosts before DSH 0.1.7 read a pixel budget (`maxPixels`), later ones the
|
|
@@ -70,8 +120,11 @@ export function imageRequestTarget(ref, limit) {
|
|
|
70
120
|
* @returns the same messages with image blocks resolved for the translators.
|
|
71
121
|
*/
|
|
72
122
|
export async function resolveImages(messages, attachments, signal, limit) {
|
|
73
|
-
const
|
|
74
|
-
|
|
123
|
+
const hasRetainedImage = (block) => block.type === 'image'
|
|
124
|
+
? !('offloaded' in block && block.offloaded === true)
|
|
125
|
+
: block.type === 'tool-result' && block.content.some(hasRetainedImage);
|
|
126
|
+
if (attachments === undefined
|
|
127
|
+
&& messages.some(message => message.content.some(hasRetainedImage))) {
|
|
75
128
|
throw new LlmError('dsh-subscriptions: the request carries an image but no attachments service is mounted; '
|
|
76
129
|
+ 'image input requires the harness attachment store', 'UNSUPPORTED');
|
|
77
130
|
}
|
|
@@ -94,8 +147,18 @@ export async function resolveImages(messages, attachments, signal, limit) {
|
|
|
94
147
|
return { data: stored.data, mediaType: stored.ref.mediaType, ref: stored.ref };
|
|
95
148
|
};
|
|
96
149
|
const resolveBlock = async (block) => {
|
|
97
|
-
if (block.type
|
|
150
|
+
if (block.type === 'tool-result') {
|
|
151
|
+
return [{ ...block, content: (await Promise.all(block.content.map(resolveBlock))).flat() }];
|
|
152
|
+
}
|
|
153
|
+
if (block.type !== 'image' || 'dataBase64' in block)
|
|
98
154
|
return [block];
|
|
155
|
+
if ('offloaded' in block && block.offloaded === true) {
|
|
156
|
+
// The host owns omission decisions. Never restore offloaded bytes, even
|
|
157
|
+
// when the original attachment is unreadable or no store is mounted.
|
|
158
|
+
const format = llm.offloadedImageText;
|
|
159
|
+
return [{ type: 'text', text: format?.(block.attachment)
|
|
160
|
+
?? `[image omitted to fit request image limits; ${block.attachment.attachmentId}]` }];
|
|
161
|
+
}
|
|
99
162
|
const { data, mediaType: sentType, ref } = await readForRequest(block.attachment);
|
|
100
163
|
const { attachmentId, mediaType, bytes, width, height, name } = ref;
|
|
101
164
|
return [{
|
|
@@ -168,15 +168,17 @@ export function mapResponsesUsage(usage) {
|
|
|
168
168
|
* @returns the mapped error (context overflow, quota, otherwise SERVER).
|
|
169
169
|
*/
|
|
170
170
|
export function responsesFailure(code, message) {
|
|
171
|
-
|
|
171
|
+
// Safe here means this diagnostic path carries no provider-controlled free text,
|
|
172
|
+
// not that no secret can exist anywhere. Raw values remain local classification inputs.
|
|
172
173
|
const detail = `${code ?? ''} ${message ?? ''}`;
|
|
174
|
+
let classification = 'SERVER';
|
|
173
175
|
if (code === 'context_window_exceeded' || isContextWindowExceededError(detail)) {
|
|
174
|
-
|
|
176
|
+
classification = CONTEXT_WINDOW_EXCEEDED_CODE;
|
|
175
177
|
}
|
|
176
|
-
if ((code !== undefined && /insufficient|quota/i.test(code)) || isQuotaExceededError(detail)) {
|
|
177
|
-
|
|
178
|
+
else if ((code !== undefined && /insufficient|quota/i.test(code)) || isQuotaExceededError(detail)) {
|
|
179
|
+
classification = QUOTA_EXCEEDED_CODE;
|
|
178
180
|
}
|
|
179
|
-
return new LlmError(
|
|
181
|
+
return new LlmError(`Responses failure (${classification}): [provider response body omitted]`, classification);
|
|
180
182
|
}
|
|
181
183
|
/** Assemble the final ContentBlock for one open block. */
|
|
182
184
|
function closeBlock(block) {
|
|
@@ -396,7 +398,7 @@ export async function* streamResponses(stream, onActivity, transform) {
|
|
|
396
398
|
event = JSON.parse(sseEvent.data);
|
|
397
399
|
}
|
|
398
400
|
catch {
|
|
399
|
-
throw new LlmError(
|
|
401
|
+
throw new LlmError('malformed SSE payload (MALFORMED_RESPONSE): [provider response body omitted]', 'MALFORMED_RESPONSE');
|
|
400
402
|
}
|
|
401
403
|
if (transform !== undefined)
|
|
402
404
|
event = transform(event);
|
package/lib/translate/sse.js
CHANGED
|
@@ -51,6 +51,14 @@ export async function* parseSse(stream, onActivity) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
finally {
|
|
54
|
+
// A terminal SSE frame can precede HTTP EOF; returning or decoding failure
|
|
55
|
+
// must close the remaining body rather than leave the connection reading.
|
|
56
|
+
try {
|
|
57
|
+
await reader.cancel();
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Cleanup failure must not mask the original stream outcome.
|
|
61
|
+
}
|
|
54
62
|
reader.releaseLock();
|
|
55
63
|
}
|
|
56
64
|
}
|