dsh-subscriptions 0.1.4 → 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.
@@ -6,12 +6,11 @@ export type ModelDefaults = Readonly<Partial<Record<ProviderId, ModelDefaultMap>
6
6
  /** Absolute path of the defaults file. */
7
7
  export declare function modelDefaultsFilePath(): string;
8
8
  /**
9
- * The last load failure, or a warning about entries that were skipped while
10
- * loading; consumers only use it for diagnostics.
9
+ * Ready the defaults store.
10
+ * index.ts calls it at apply time so every later synchronous read sees the
11
+ * persisted state; store tests also call it after resetting the singleton.
11
12
  */
12
- export declare function modelDefaultsLoadError(): unknown;
13
- /** A detached snapshot for the RPC surface (render + diffing). */
14
- export declare function modelDefaultsSnapshot(): ModelDefaults;
13
+ export declare function loadModelDefaults(): Promise<void>;
15
14
  /**
16
15
  * Set or clear one model's configured default effort, then persist. The
17
16
  * memory snapshot updates only after the atomic write succeeds, so a failed
@@ -8,10 +8,10 @@
8
8
  * chip the model picker shows when the discovered catalog advertises no
9
9
  * default at all.
10
10
  *
11
- * Writes are single-process and atomic, but *not* as serialised as the rest
12
- * of the page: the Settings page disables only the row being saved, so two
13
- * rows saved back to back can overlap. The write chain below serialises them,
14
- * so no update is lost to a read-modify-write race. Every read comes from the
11
+ * Writes are single-process and atomic. The account manager submits effort
12
+ * edits sequentially and disables its editor while saving, but concurrent RPC
13
+ * callers can still overlap. The write chain below serialises them, so no
14
+ * update is lost to a read-modify-write race. Every read comes from the
15
15
  * in-memory snapshot, so the on-disk file only needs to survive a restart: a
16
16
  * malformed file reads as empty and is rewritten on the next save, never
17
17
  * taking the plugin down with it.
@@ -29,21 +29,21 @@ const EMPTY = Object.freeze({});
29
29
  let current = EMPTY;
30
30
  /** One lazy load of the on-disk file (read once per process). */
31
31
  let ready;
32
- /** Last load failure, surfaced to callers that care; defaults stay empty. */
32
+ /** Last load failure or skipped-entry warning, inspected only by tests. */
33
33
  let loadError;
34
34
  /**
35
35
  * Serialises every write: the read-modify-write sequence must not interleave,
36
36
  * or a fast second save would compute its snapshot from the stale `current`
37
- * and silently drop the first update (the UI disables only the row being
38
- * saved, so overlaps are reachable).
37
+ * and silently drop the first update. Concurrent RPC callers can overlap
38
+ * even though one account-manager dialog submits its edits sequentially.
39
39
  */
40
40
  let writeChain = Promise.resolve();
41
41
  /**
42
42
  * Validate one persisted provider section: a string→string map, or undefined.
43
43
  * Malformed *entries* are skipped, not the whole section: one bad value (a
44
44
  * hand edit losing its quotes) must not silently un-configure every model in
45
- * that provider. What was dropped is reported so the caller can surface it
46
- * instead of the loss disappearing.
45
+ * that provider. Dropped model ids are recorded in a test-only load warning;
46
+ * production callers do not surface this warning.
47
47
  */
48
48
  function sanitizeProvider(value, dropped) {
49
49
  if (typeof value !== 'object' || value === null || Array.isArray(value))
@@ -136,15 +136,16 @@ function sectionOf(defaults, provider) {
136
136
  }
137
137
  /**
138
138
  * Ready the defaults store.
139
- * @internal Exported for tests; index.ts calls it at apply time so every
140
- * later synchronous read sees the persisted state.
139
+ * index.ts calls it at apply time so every later synchronous read sees the
140
+ * persisted state; store tests also call it after resetting the singleton.
141
141
  */
142
142
  export async function loadModelDefaults() {
143
143
  await ensureReady();
144
144
  }
145
145
  /**
146
- * The last load failure, or a warning about entries that were skipped while
147
- * loading; consumers only use it for diagnostics.
146
+ * Test-only observation of the last load failure or skipped-entry warning.
147
+ * No production diagnostic consumer currently reads it.
148
+ * @internal Exported for tests only.
148
149
  */
149
150
  export function modelDefaultsLoadError() {
150
151
  return loadError;
@@ -164,7 +165,10 @@ export function defaultEffortOf(provider, model) {
164
165
  // breaks that model's resolution).
165
166
  return Object.prototype.hasOwnProperty.call(section, model) ? section[model] : undefined;
166
167
  }
167
- /** A detached snapshot for the RPC surface (render + diffing). */
168
+ /**
169
+ * Detached snapshot for store assertions in tests; no production RPC uses it.
170
+ * @internal Exported for tests only.
171
+ */
168
172
  export function modelDefaultsSnapshot() {
169
173
  const result = {};
170
174
  for (const provider of PROVIDER_IDS) {
@@ -60,8 +60,6 @@ export declare function isAntigravityPermanentRefreshError(error: unknown): bool
60
60
  export declare function fetchAntigravityModels(session: AntigravitySession, runtime?: AntigravityRuntimeConfig, fetchFn?: FetchFn, signal?: AbortSignal): Promise<DiscoveredModel[]>;
61
61
  /** Fetch plan and per-model quota windows when the upstream exposes them. */
62
62
  export declare function fetchAntigravityUsage(session: AntigravitySession, runtime?: AntigravityRuntimeConfig, fetchFn?: FetchFn, signal?: AbortSignal): Promise<ProviderUsage>;
63
- /** URL for either v1internal generation transport. */
64
- export declare function antigravityGenerateURL(baseURL: string | undefined, stream: boolean): string;
65
63
  /** Forward one already-built payload to generateContent or streamGenerateContent. */
66
64
  export declare function requestAntigravityContent(session: AntigravitySession, payload: AntigravityRequest, stream: boolean, runtime?: AntigravityRuntimeConfig, fetchFn?: FetchFn, signal?: AbortSignal): Promise<Response>;
67
65
  export interface AntigravityAdapterOptions {
@@ -7,7 +7,7 @@ import { errorChain, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError } from '@deepseek
7
7
  import { resolveImages } from '../translate/resolved.js';
8
8
  import { antigravityReasoning } from '../translate/antigravity-thinking.js';
9
9
  import { parseAntigravityResponse, streamAntigravity, toAntigravityRequest, } from '../translate/antigravity.js';
10
- import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverOrRetryAuth, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, discoverAcrossAccounts, isDiscoveryAborted, } from './common.js';
10
+ import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverOrRetryAuth, isMissingOrInvalidCredential, parseProviderJson, oauthEndpointError, OAuthEndpointError, discoverAcrossAccounts, isDiscoveryAborted, } from './common.js';
11
11
  import { hostFetch } from '../http.js';
12
12
  import { ANTIGRAVITY_DEFAULT_CLIENT_ID, ANTIGRAVITY_DEFAULT_CLIENT_SECRET } from './antigravity-oauth-client.js';
13
13
  import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
@@ -126,7 +126,7 @@ async function callInternal(method, body, accessToken, runtime, fetchFn, signal)
126
126
  }, runtime, fetchFn);
127
127
  if (!response.ok)
128
128
  throw await httpLlmError(response, `Antigravity ${method}`);
129
- return response.json();
129
+ return parseProviderJson(response, `Antigravity ${method}`);
130
130
  }
131
131
  function projectIdOf(value) {
132
132
  if (typeof value === 'string' && value.length > 0)
@@ -217,7 +217,7 @@ export async function exchangeAntigravityCode(code, verifier, redirectUri, oauth
217
217
  });
218
218
  if (!response.ok)
219
219
  throw await oauthEndpointError(response, 'Antigravity');
220
- const tokens = await response.json();
220
+ const tokens = await parseProviderJson(response, 'Antigravity token exchange');
221
221
  if (typeof tokens.access_token !== 'string')
222
222
  throw new Error('Antigravity token endpoint returned no access token');
223
223
  const account = await discoverAntigravityAccount(tokens.access_token, runtime, fetchFn);
@@ -238,7 +238,7 @@ export async function refreshAntigravity(session, oauth, fetchFn = hostFetch) {
238
238
  });
239
239
  if (!response.ok)
240
240
  throw await oauthEndpointError(response, 'Antigravity');
241
- return sessionFromTokens(await response.json(), {
241
+ return sessionFromTokens(await parseProviderJson(response, 'Antigravity token refresh'), {
242
242
  projectId: session.projectId,
243
243
  ...session.account === undefined ? {} : { account: session.account },
244
244
  ...session.plan === undefined ? {} : { plan: session.plan },
@@ -313,10 +313,6 @@ export async function fetchAntigravityUsage(session, runtime = {}, fetchFn = hos
313
313
  ...displayPlan === undefined ? {} : { plan: displayPlan },
314
314
  };
315
315
  }
316
- /** URL for either v1internal generation transport. */
317
- export function antigravityGenerateURL(baseURL, stream) {
318
- return `${antigravityBaseURL(baseURL)}/v1internal:${stream ? 'streamGenerateContent?alt=sse' : 'generateContent'}`;
319
- }
320
316
  /** Forward one already-built payload to generateContent or streamGenerateContent. */
321
317
  export async function requestAntigravityContent(session, payload, stream, runtime = {}, fetchFn = hostFetch, signal) {
322
318
  return fetchAntigravity(stream ? 'streamGenerateContent?alt=sse' : 'generateContent', {
@@ -511,6 +507,6 @@ export class AntigravityAdapter extends LlmAdapter {
511
507
  const response = await requestAntigravityContent(session, toAntigravityRequest(options, messages, session.projectId), false, this.options.runtime, this.options.fetchFn, options.signal);
512
508
  if (!response.ok)
513
509
  throw await httpLlmError(response, 'Antigravity API');
514
- return parseAntigravityResponse(await response.json());
510
+ return parseAntigravityResponse(await parseProviderJson(response, 'Antigravity generateContent'));
515
511
  }
516
512
  }
@@ -7,7 +7,7 @@ import { execFileSync } from 'node:child_process';
7
7
  import { EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
8
8
  import { hostSupportsImageOffload, IMAGE_OFFLOAD_REQUIRED, requiredImageOffloadCount, resolveImages } from '../translate/resolved.js';
9
9
  import { markMessageCache, streamAnthropic, toAnthropicMessages, toAnthropicSystem, toAnthropicTools, } from '../translate/anthropic.js';
10
- import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
10
+ import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, parseProviderJson, oauthEndpointError, OAuthEndpointError, } from './common.js';
11
11
  import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
12
12
  import { hostFetch } from '../http.js';
13
13
  import { compareVersions } from './npm-cli-version.js';
@@ -212,7 +212,7 @@ export async function exchangeClaudeCode(code, verifier, redirectUri, state) {
212
212
  });
213
213
  if (!response.ok)
214
214
  throw await oauthEndpointError(response, 'claude');
215
- return claudeSession(await response.json(), undefined, true);
215
+ return claudeSession(await parseProviderJson(response, 'claude token exchange'), undefined, true);
216
216
  }
217
217
  /**
218
218
  * Refresh a claude session (JSON grant echoing the issued scope).
@@ -232,7 +232,7 @@ export async function refreshClaude(session) {
232
232
  });
233
233
  if (!response.ok)
234
234
  throw await oauthEndpointError(response, 'claude');
235
- const next = await claudeSession(await response.json(), session.refreshToken, false);
235
+ const next = await claudeSession(await parseProviderJson(response, 'claude token refresh'), session.refreshToken, false);
236
236
  return {
237
237
  ...next,
238
238
  ...session.emailAddress === undefined ? {} : { emailAddress: session.emailAddress },
@@ -398,7 +398,7 @@ export async function fetchClaudeUsage(session, fetchFn = hostFetch, signal, cli
398
398
  }
399
399
  if (!response.ok)
400
400
  throw await oauthEndpointError(response, 'claude usage');
401
- const payload = await response.json();
401
+ const payload = await parseProviderJson(response, 'claude usage');
402
402
  const resetCredits = resetCreditsError === undefined ? claudeResetCredits(payload.cedar_ember, Date.now()) : undefined;
403
403
  const resets = {
404
404
  ...resetCredits === undefined ? {} : { resetCredits },
@@ -462,7 +462,7 @@ export async function fetchClaudeModels(session, fetchFn = hostFetch, signal, cl
462
462
  });
463
463
  if (!response.ok)
464
464
  throw await httpLlmError(response, 'claude models API');
465
- const payload = await response.json();
465
+ const payload = await parseProviderJson(response, 'claude models API');
466
466
  if (!Array.isArray(payload.data)) {
467
467
  throw new Error('claude models API returned an invalid catalog');
468
468
  }
@@ -752,7 +752,7 @@ export class ClaudeAdapter extends LlmAdapter {
752
752
  if (hostSupportsImageOffload()) {
753
753
  const offloadImages = requiredImageOffloadCount(messages, CLAUDE_REQUEST_IMAGE_BUDGET);
754
754
  if (offloadImages > 0) {
755
- // Older dsh-llm option types lack `offloadImages`; 0.2+ hosts read it.
755
+ // The supported 0.1.7-rc.2 floor reads `offloadImages`; older option types lack it.
756
756
  throw new LlmError(`claude request images exceed the ${CLAUDE_REQUEST_IMAGE_BUDGET}-byte base64 budget; `
757
757
  + `${offloadImages} more oldest image(s) must be offloaded`, IMAGE_OFFLOAD_REQUIRED, { offloadImages });
758
758
  }
@@ -86,8 +86,11 @@ export class CodexWebSearchProvider {
86
86
  try {
87
87
  return JSON.parse(text);
88
88
  }
89
- catch (cause) {
90
- throw new WebError('Codex Web Search returned invalid JSON', 'CODEX_SEARCH_RESPONSE', { cause });
89
+ catch {
90
+ // No cause: the host expands causes recursively, and Node's parse error
91
+ // quotes a bounded excerpt of the body, so attaching it would relocate
92
+ // the leak rather than remove it.
93
+ throw new WebError('Codex Web Search returned invalid JSON', 'CODEX_SEARCH_RESPONSE');
91
94
  }
92
95
  }
93
96
  throw new WebError('Codex Web Search exhausted its retry policy', 'CODEX_SEARCH_UPSTREAM');
@@ -8,7 +8,7 @@ import { attributionHeaders, EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmErr
8
8
  import { decodeJwtPayload } from '../auth/jwt.js';
9
9
  import { resolveImages } from '../translate/resolved.js';
10
10
  import { streamResponses, toResponsesInput, toResponsesTools } from '../translate/responses.js';
11
- import { deterministicSessionId, effortDisplayName, httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isDiscoveryAuthFailure, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
11
+ import { deterministicSessionId, effortDisplayName, httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isDiscoveryAuthFailure, isMissingOrInvalidCredential, parseProviderJson, oauthEndpointError, OAuthEndpointError, } from './common.js';
12
12
  import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
13
13
  import { hostFetch } from '../http.js';
14
14
  import { DEFAULT_RATE_LIMIT_WAIT, DEFAULT_RETRY, jsonBody, resetFromFields, subscriptionRetryPolicy, } from './rate-limit.js';
@@ -185,7 +185,7 @@ export async function exchangeCodexCode(code, verifier, redirectUri) {
185
185
  });
186
186
  if (!response.ok)
187
187
  throw await oauthEndpointError(response, 'codex');
188
- return codexSession(await response.json());
188
+ return codexSession(await parseProviderJson(response, 'codex token exchange'));
189
189
  }
190
190
  /**
191
191
  * Refresh a codex session (JSON grant — unlike the code exchange).
@@ -204,7 +204,7 @@ export async function refreshCodex(session) {
204
204
  });
205
205
  if (!response.ok)
206
206
  throw await oauthEndpointError(response, 'codex');
207
- return codexSession(await response.json(), session);
207
+ return codexSession(await parseProviderJson(response, 'codex token refresh'), session);
208
208
  }
209
209
  /**
210
210
  * Whether a codex refresh failure means the login is permanently gone.
@@ -239,7 +239,7 @@ export async function fetchCodexResetCredits(session, fetchFn = hostFetch, signa
239
239
  });
240
240
  if (!response.ok)
241
241
  throw await oauthEndpointError(response, 'codex reset credits');
242
- const payload = await response.json();
242
+ const payload = await parseProviderJson(response, 'codex reset credits');
243
243
  if (!Array.isArray(payload?.credits))
244
244
  throw new Error('codex reset credits: unexpected response (missing credits array)');
245
245
  return payload.credits.flatMap((value) => {
@@ -269,7 +269,7 @@ export async function consumeCodexResetCredit(session, creditId, requestId, fetc
269
269
  });
270
270
  if (!response.ok)
271
271
  throw await oauthEndpointError(response, 'codex consume reset');
272
- const payload = await response.json();
272
+ const payload = await parseProviderJson(response, 'codex consume reset');
273
273
  if (payload?.code !== 'reset')
274
274
  throw new Error('Reset was not confirmed. Verify the account in Codex before retrying.');
275
275
  }
@@ -347,7 +347,7 @@ export async function fetchCodexUsage(session, fetchFn = hostFetch, signal) {
347
347
  });
348
348
  if (!response.ok)
349
349
  throw await oauthEndpointError(response, 'codex usage');
350
- const payload = await response.json();
350
+ const payload = await parseProviderJson(response, 'codex usage');
351
351
  const windows = [];
352
352
  const primary = codexUsageWindow(payload.rate_limit?.primary_window, 'session');
353
353
  const secondary = codexUsageWindow(payload.rate_limit?.secondary_window, 'weekly');
@@ -410,7 +410,7 @@ export async function fetchCodexModels(session, fetchFn = hostFetch, signal, cli
410
410
  });
411
411
  if (!response.ok)
412
412
  throw await oauthEndpointError(response, 'codex models');
413
- const payload = await response.json();
413
+ const payload = await parseProviderJson(response, 'codex models');
414
414
  if (!Array.isArray(payload.models))
415
415
  throw new Error('codex models endpoint returned no models array');
416
416
  const discovered = [];
@@ -33,6 +33,19 @@ export interface ModelEntry {
33
33
  * @returns the validated entries.
34
34
  */
35
35
  export declare function validateModels(models: readonly ModelEntry[], label: string): ModelEntry[];
36
+ /**
37
+ * Parse a provider response or stored JSON string without exposing input excerpts.
38
+ * Success returns the same JSON value; only SyntaxError is replaced, preserving
39
+ * parse-failure type checks and leaving body-read/abort failures unchanged.
40
+ * Node's Unexpected token errors include a bounded input excerpt. Do not retain
41
+ * the original error as cause: the host displays causes recursively.
42
+ * @param input - response to consume once, or JSON text to parse.
43
+ * @param origin - local endpoint/operation label; never response.url, a discovered
44
+ * URL, a credential, or any other provider-controlled text.
45
+ * @returns the parsed value, without validation or credential filtering.
46
+ * @throws SyntaxError with the local origin and a fixed omission marker.
47
+ */
48
+ export declare function parseProviderJson<T = unknown>(input: Response | string, origin: string): Promise<T>;
36
49
  /** Optional per-call hooks {@link httpLlmError} uses to read a rate-limit window. */
37
50
  export interface HttpLlmErrorOptions {
38
51
  /**
@@ -61,6 +61,28 @@ export function validateModels(models, label) {
61
61
  function providerDiagnostic(summary, hasBody) {
62
62
  return hasBody ? `${summary}: [provider response body omitted]` : summary;
63
63
  }
64
+ /**
65
+ * Parse a provider response or stored JSON string without exposing input excerpts.
66
+ * Success returns the same JSON value; only SyntaxError is replaced, preserving
67
+ * parse-failure type checks and leaving body-read/abort failures unchanged.
68
+ * Node's Unexpected token errors include a bounded input excerpt. Do not retain
69
+ * the original error as cause: the host displays causes recursively.
70
+ * @param input - response to consume once, or JSON text to parse.
71
+ * @param origin - local endpoint/operation label; never response.url, a discovered
72
+ * URL, a credential, or any other provider-controlled text.
73
+ * @returns the parsed value, without validation or credential filtering.
74
+ * @throws SyntaxError with the local origin and a fixed omission marker.
75
+ */
76
+ export async function parseProviderJson(input, origin) {
77
+ try {
78
+ return (typeof input === 'string' ? JSON.parse(input) : await input.json());
79
+ }
80
+ catch (error) {
81
+ if (!(error instanceof SyntaxError))
82
+ throw error;
83
+ throw new SyntaxError(providerDiagnostic(`${origin}: invalid JSON`, true));
84
+ }
85
+ }
64
86
  /**
65
87
  * Build an LlmError from a non-2xx provider response, mapping the status to a
66
88
  * stable code and, for a rate-limited request, the disclosed reset instant to
@@ -17,7 +17,7 @@ import { EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, ReasoningEffortI
17
17
  import { resolveImages } from '../translate/resolved.js';
18
18
  import { streamChatCompletions, toChatMessages, toChatTools, } from '../translate/chat-completions.js';
19
19
  import { streamResponses, toResponsesInput, toResponsesTools } from '../translate/responses.js';
20
- import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
20
+ import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, parseProviderJson, oauthEndpointError, OAuthEndpointError, } from './common.js';
21
21
  import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
22
22
  import { hostFetch } from '../http.js';
23
23
  import { DEFAULT_RATE_LIMIT_WAIT, DEFAULT_RETRY, subscriptionRetryPolicy, } from './rate-limit.js';
@@ -144,7 +144,7 @@ export async function exchangeCopilotToken(githubToken, fetchFn = hostFetch) {
144
144
  });
145
145
  if (!response.ok)
146
146
  throw await oauthEndpointError(response, 'copilot');
147
- const wire = await response.json();
147
+ const wire = await parseProviderJson(response, 'copilot token exchange');
148
148
  if (typeof wire.token !== 'string' || wire.token.length === 0) {
149
149
  throw new Error('copilot token endpoint returned no token');
150
150
  }
@@ -268,7 +268,7 @@ export async function fetchCopilotModels(session, fetchFn = hostFetch, signal) {
268
268
  });
269
269
  if (!response.ok)
270
270
  throw await oauthEndpointError(response, 'copilot models');
271
- const payload = await response.json();
271
+ const payload = await parseProviderJson(response, 'copilot models');
272
272
  if (!Array.isArray(payload.data))
273
273
  throw new Error('copilot models endpoint returned no data array');
274
274
  const seen = new Set();
@@ -5,8 +5,43 @@ import { CursorAdapter } from 'dsh-subscriptions/cursor-transport';
5
5
  * blocks. DSH 0.1.7 carries them as first-class role=tool messages instead.
6
6
  */
7
7
  export declare function projectCursorMessages(messages: GenerateOptions['messages']): unknown[];
8
+ /**
9
+ * Cursor adapter for the DSH host.
10
+ *
11
+ * Failure text contract: `finish/error` messages that originate from the
12
+ * vendor stream and can quote provider-controlled text (the response content
13
+ * type, trailers, end-stream errors, a frame-reader or other error whose message
14
+ * is not one of the literals below, and a transport-coded failure that is not a
15
+ * socket-layer error) are replaced by `<code> [provider response text omitted]`. Failures that are
16
+ * wholly local keep their own message and code:
17
+ *
18
+ * - anything the auth service threw (not signed in, sign-in expired, token
19
+ * refresh status), and a host attachment read failure;
20
+ * - the vendor's own request validation, by code (`UNSUPPORTED_OPTION`,
21
+ * `UNSUPPORTED_CONTENT`, `TOOL_LIMIT`);
22
+ * - the vendor's fixed-literal plain errors, by exact message: the HTTP status
23
+ * (`Cursor agent returned HTTP 429`), the response, idle and progress
24
+ * timeouts, a closed bridge or stream, an unreadable compressed frame, and
25
+ * an invalid Cursor setting;
26
+ * - a network error raised by the operating system (`ECONNREFUSED`,
27
+ * `ENOTFOUND`, ...), recognised from the thrown error's own code, not from
28
+ * the vendor's classification.
29
+ *
30
+ * Not a provider-origin class: a network error such as `ECONNREFUSED` is a
31
+ * local failure (no network, daemon down, DNS), and is kept. What is still
32
+ * replaced although it is probably local is a transport failure with no
33
+ * socket-layer errno: a TLS or certificate failure and an HTTP/2 protocol
34
+ * error. The vendor's `TRANSPORT` code cannot decide those, because
35
+ * `classifyCursorError` derives it from message text (`network`, `connection`,
36
+ * `socket`, `fetch`, `ECONN`, `http2`) and a provider's end-stream message or
37
+ * `grpc-message` can contain any of them; so the code is never trusted alone.
38
+ * Telling these apart would need a check on the thrown error object, as the
39
+ * errno check above does, extended to the TLS and HTTP/2 error codes. That is
40
+ * not done here and is not verified.
41
+ */
8
42
  export declare class CursorCompatAdapter extends CursorAdapter {
9
43
  private readonly visibleModels;
44
+ private readonly localFailures;
10
45
  constructor(options: ConstructorParameters<typeof CursorAdapter>[0] & {
11
46
  visibleModels?: () => readonly string[] | undefined;
12
47
  });