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.
Files changed (75) hide show
  1. package/README.md +64 -0
  2. package/lib/auth/device-flow.js +6 -3
  3. package/lib/auth/oauth-flow.js +41 -8
  4. package/lib/auth/rpc.d.ts +3 -1
  5. package/lib/auth/rpc.js +10 -1
  6. package/lib/client/ProviderAccountManager.js +34 -41
  7. package/lib/client/ProviderModelEditor.js +5 -9
  8. package/lib/client/SpeedSelect.js +8 -7
  9. package/lib/client/SubscriptionUsageBadge.js +6 -5
  10. package/lib/client/SubscriptionsSection.d.ts +21 -73
  11. package/lib/client/SubscriptionsSection.js +130 -104
  12. package/lib/client/UsageBadgeDisplaySetting.js +4 -8
  13. package/lib/client/host-contract.d.ts +1 -1
  14. package/lib/client/host-contract.js +22 -0
  15. package/lib/client/locales.d.ts +58 -0
  16. package/lib/client/locales.js +58 -0
  17. package/lib/client/provider-settings-styles.d.ts +3 -0
  18. package/lib/client/provider-settings-styles.js +206 -0
  19. package/lib/client/reset-action.d.ts +14 -0
  20. package/lib/client/reset-action.js +15 -0
  21. package/lib/client/reset-credits-view.d.ts +53 -0
  22. package/lib/client/reset-credits-view.js +103 -0
  23. package/lib/client.js +937 -310
  24. package/lib/client.js.map +1 -1
  25. package/lib/index.d.ts +9 -0
  26. package/lib/index.js +6058 -5298
  27. package/lib/model-defaults.d.ts +4 -5
  28. package/lib/model-defaults.js +18 -14
  29. package/lib/providers/account-preferences.js +3 -2
  30. package/lib/providers/antigravity.d.ts +0 -2
  31. package/lib/providers/antigravity.js +5 -9
  32. package/lib/providers/catalog-store.d.ts +3 -2
  33. package/lib/providers/catalog-store.js +57 -26
  34. package/lib/providers/claude.d.ts +22 -3
  35. package/lib/providers/claude.js +118 -20
  36. package/lib/providers/codex-search.js +5 -2
  37. package/lib/providers/codex.d.ts +15 -2
  38. package/lib/providers/codex.js +77 -9
  39. package/lib/providers/common.d.ts +38 -0
  40. package/lib/providers/common.js +44 -18
  41. package/lib/providers/copilot.d.ts +2 -1
  42. package/lib/providers/copilot.js +13 -6
  43. package/lib/providers/cursor-adapter.d.ts +35 -0
  44. package/lib/providers/cursor-adapter.js +245 -4
  45. package/lib/providers/cursor-auth.js +11 -4
  46. package/lib/providers/cursor-usage.js +2 -1
  47. package/lib/providers/external-usage.js +2 -1
  48. package/lib/providers/grok.d.ts +2 -2
  49. package/lib/providers/grok.js +16 -11
  50. package/lib/providers/minimax-usage.js +2 -1
  51. package/lib/providers/pool-health.d.ts +2 -0
  52. package/lib/providers/pool-health.js +9 -0
  53. package/lib/providers/pool-usage.d.ts +13 -1
  54. package/lib/providers/pool-usage.js +39 -7
  55. package/lib/providers/pool.d.ts +7 -5
  56. package/lib/providers/pool.js +89 -12
  57. package/lib/providers/rate-limit.d.ts +0 -14
  58. package/lib/providers/rate-limit.js +0 -26
  59. package/lib/providers/replay.d.ts +4 -0
  60. package/lib/providers/replay.js +50 -0
  61. package/lib/providers/reset-redemption.d.ts +51 -0
  62. package/lib/providers/reset-redemption.js +102 -0
  63. package/lib/tools/image-generate.js +2 -1
  64. package/lib/tools/video-generate.d.ts +0 -5
  65. package/lib/tools/video-generate.js +25 -14
  66. package/lib/tools/x-search.js +2 -2
  67. package/lib/translate/anthropic.d.ts +21 -2
  68. package/lib/translate/anthropic.js +35 -11
  69. package/lib/translate/antigravity.js +13 -6
  70. package/lib/translate/chat-completions.js +1 -1
  71. package/lib/translate/resolved.d.ts +24 -0
  72. package/lib/translate/resolved.js +66 -3
  73. package/lib/translate/responses.js +8 -6
  74. package/lib/translate/sse.js +8 -0
  75. 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 = { type: 'ephemeral' };
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
- blocks[blocks.length - 1].cache_control = { type: 'ephemeral' };
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
- return new LlmError(message, CONTEXT_WINDOW_EXCEEDED_CODE);
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
- if (type === 'rate_limit_error')
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(`malformed SSE payload: ${sseEvent.data.slice(0, 120)}`, 'MALFORMED_RESPONSE');
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
- * Only a non-array JSON object survives verbatim; every other JSON value is
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
- return parsed;
33
- return { output: parsed };
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({ type: 'tool-result', toolCallId: ToolCallId(id), content: message.content }),
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(`malformed Antigravity SSE payload: ${event.data.slice(0, 120)}`, 'MALFORMED_RESPONSE');
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(`malformed SSE payload: ${sseEvent.data.slice(0, 120)}`, 'MALFORMED_RESPONSE');
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 hasImage = messages.some(message => message.content.some(block => block.type === 'image'));
74
- if (hasImage && attachments === undefined) {
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 !== 'image')
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
- const text = message ?? code ?? 'the provider reported a failed response';
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
- return new LlmError(text, CONTEXT_WINDOW_EXCEEDED_CODE);
176
+ classification = CONTEXT_WINDOW_EXCEEDED_CODE;
175
177
  }
176
- if ((code !== undefined && /insufficient|quota/i.test(code)) || isQuotaExceededError(detail)) {
177
- return new LlmError(text, QUOTA_EXCEEDED_CODE);
178
+ else if ((code !== undefined && /insufficient|quota/i.test(code)) || isQuotaExceededError(detail)) {
179
+ classification = QUOTA_EXCEEDED_CODE;
178
180
  }
179
- return new LlmError(text, 'SERVER');
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(`malformed SSE payload: ${sseEvent.data.slice(0, 120)}`, 'MALFORMED_RESPONSE');
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);
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-subscriptions",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Unified subscription providers and usage dashboard for DeepSeek Harness",
5
5
  "icon": "./icon.svg",
6
6
  "license": "MIT",