Skip to content

Commit 788b1d7

Browse files
authored
fix(provider): improve DeepSeek request failure handling (#104)
Surface DeepSeek HTTP and network failures with user-facing messages instead of a generic fetch failure. - add structured request error normalization and diagnostics - add provider-aware action links for API key, usage, status, and logs - route chat error actions through VS Code URI handlers - split client and runtime activation code into focused modules
1 parent 3bae94b commit 788b1d7

18 files changed

Lines changed: 879 additions & 156 deletions

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"typescript.tsdk": "node_modules/typescript/lib",
2+
"js/ts.tsdk.path": "node_modules/typescript/lib",
33
"editor.codeActionsOnSave": {
44
"source.organizeImports": "explicit"
55
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"chat"
3636
],
3737
"activationEvents": [
38+
"onUri",
3839
"onStartupFinished"
3940
],
4041
"enabledApiProposals": [],

src/client/consts.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import type {
2+
ApiProviderId,
3+
HttpErrorLinkDefinition,
4+
HttpErrorLinkStatusKey,
5+
NetworkErrorCategory,
6+
} from './types';
7+
import { EXTERNAL_URLS } from '../consts';
8+
9+
export const OFFICIAL_DEEPSEEK_API_HOST = 'api.deepseek.com';
10+
export const MAX_DIAGNOSTIC_FIELD_LENGTH = 300;
11+
12+
export const API_PROVIDER_HTTP_ERROR_LINKS: Readonly<
13+
Record<HttpErrorLinkStatusKey, Readonly<Partial<Record<ApiProviderId, HttpErrorLinkDefinition>>>>
14+
> = {
15+
401: {
16+
deepseek: {
17+
labelKey: 'error.action.createApiKey',
18+
url: EXTERNAL_URLS.deepseek.apiKeys,
19+
},
20+
},
21+
402: {
22+
deepseek: {
23+
labelKey: 'error.action.viewUsage',
24+
url: EXTERNAL_URLS.deepseek.usage,
25+
},
26+
},
27+
'5xx': {
28+
deepseek: {
29+
labelKey: 'error.action.checkDeepSeekStatus',
30+
url: EXTERNAL_URLS.deepseek.status,
31+
},
32+
},
33+
};
34+
35+
/**
36+
* Curated network error codes observed from Node.js fetch failures.
37+
*
38+
* Sources: Node errno / c-ares DNS codes (`NodeJS.ErrnoException.code`),
39+
* Node TLS/OpenSSL error codes, and undici error `code` / `name` literals
40+
* from the `undici-types` package bundled through `@types/node`.
41+
*
42+
* This is intentionally not exhaustive: unknown codes fall back to `generic`
43+
* while still being shown to the user in the error message.
44+
*/
45+
export const NETWORK_ERROR_CATEGORY_BY_CODE = {
46+
ENOTFOUND: 'dns',
47+
EAI_AGAIN: 'dns',
48+
ENODATA: 'dns',
49+
ESERVFAIL: 'dns',
50+
EFORMERR: 'dns',
51+
ENONAME: 'dns',
52+
EBADNAME: 'dns',
53+
EBADQUERY: 'dns',
54+
EBADFAMILY: 'dns',
55+
EBADRESP: 'dns',
56+
ENOTIMP: 'dns',
57+
EREFUSED: 'dns',
58+
ENOTINITIALIZED: 'dns',
59+
ELOADIPHLPAPI: 'dns',
60+
EADDRGETNETWORKPARAMS: 'dns',
61+
ECONNREFUSED: 'unreachable',
62+
ENETUNREACH: 'unreachable',
63+
EHOSTUNREACH: 'unreachable',
64+
EADDRNOTAVAIL: 'unreachable',
65+
ENETDOWN: 'unreachable',
66+
EHOSTDOWN: 'unreachable',
67+
ECONNRESET: 'interrupted',
68+
ECONNABORTED: 'interrupted',
69+
ENETRESET: 'interrupted',
70+
ENOTCONN: 'interrupted',
71+
EPIPE: 'interrupted',
72+
EOF: 'interrupted',
73+
UND_ERR_SOCKET: 'interrupted',
74+
SocketError: 'interrupted',
75+
ETIMEDOUT: 'timeout',
76+
ETIMEOUT: 'timeout',
77+
ESOCKETTIMEDOUT: 'timeout',
78+
UND_ERR_CONNECT_TIMEOUT: 'timeout',
79+
UND_ERR_HEADERS_TIMEOUT: 'timeout',
80+
UND_ERR_BODY_TIMEOUT: 'timeout',
81+
ERR_TLS_HANDSHAKE_TIMEOUT: 'timeout',
82+
TimeoutError: 'timeout',
83+
ConnectTimeoutError: 'timeout',
84+
HeadersTimeoutError: 'timeout',
85+
BodyTimeoutError: 'timeout',
86+
CERT_HAS_EXPIRED: 'tls',
87+
CERT_NOT_YET_VALID: 'tls',
88+
CERT_UNTRUSTED: 'tls',
89+
CERT_REJECTED: 'tls',
90+
CERT_SIGNATURE_FAILURE: 'tls',
91+
SELF_SIGNED_CERT_IN_CHAIN: 'tls',
92+
DEPTH_ZERO_SELF_SIGNED_CERT: 'tls',
93+
UNABLE_TO_VERIFY_LEAF_SIGNATURE: 'tls',
94+
UNABLE_TO_GET_ISSUER_CERT_LOCALLY: 'tls',
95+
UNABLE_TO_GET_ISSUER_CERT: 'tls',
96+
UNABLE_TO_GET_CRL: 'tls',
97+
UNABLE_TO_DECRYPT_CERT_SIGNATURE: 'tls',
98+
UNABLE_TO_DECRYPT_CRL_SIGNATURE: 'tls',
99+
UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY: 'tls',
100+
CRL_SIGNATURE_FAILURE: 'tls',
101+
ERR_TLS_CERT_ALTNAME_INVALID: 'tls',
102+
UND_ERR_PRX_TLS: 'tls',
103+
SecureProxyConnectionError: 'tls',
104+
ABORT_ERR: 'aborted',
105+
AbortError: 'aborted',
106+
UND_ERR_ABORTED: 'aborted',
107+
ECANCELLED: 'aborted',
108+
UND_ERR_HEADERS_OVERFLOW: 'protocol',
109+
UND_ERR_RESPONSE: 'protocol',
110+
UND_ERR_REQ_CONTENT_LENGTH_MISMATCH: 'protocol',
111+
UND_ERR_RES_CONTENT_LENGTH_MISMATCH: 'protocol',
112+
UND_ERR_RES_EXCEEDED_MAX_SIZE: 'protocol',
113+
HTTPParserError: 'protocol',
114+
HeadersOverflowError: 'protocol',
115+
ResponseError: 'protocol',
116+
ResponseContentLengthMismatchError: 'protocol',
117+
ResponseExceededMaxSizeError: 'protocol',
118+
ERR_INVALID_URL: 'configuration',
119+
ERR_INVALID_ARG_TYPE: 'configuration',
120+
ERR_INVALID_ARG_VALUE: 'configuration',
121+
UND_ERR_INVALID_ARG: 'configuration',
122+
InvalidArgumentError: 'configuration',
123+
} as const satisfies Record<string, NetworkErrorCategory>;

src/client.ts renamed to src/client/core.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import type { CancellationToken } from 'vscode';
2-
import { safeStringify } from './json';
3-
import { logger } from './logger';
2+
import { safeStringify } from '../json';
3+
import { logger } from '../logger';
44
import type {
55
DeepSeekRequest,
66
DeepSeekStreamChunk,
77
DeepSeekToolCall,
88
StreamCallbacks,
9-
} from './types';
9+
} from '../types';
10+
import { createHttpError, normalizeRequestError } from './error';
1011

1112
/**
1213
* Lightweight SSE-streaming DeepSeek API client.
@@ -53,15 +54,7 @@ export class DeepSeekClient {
5354
});
5455

5556
if (!response.ok) {
56-
const errorText = await response.text();
57-
let errorMessage: string;
58-
try {
59-
const errorJson = JSON.parse(errorText);
60-
errorMessage = errorJson.error?.message || errorJson.message || errorText;
61-
} catch {
62-
errorMessage = errorText;
63-
}
64-
throw new Error(`DeepSeek API error (${response.status}): ${errorMessage}`);
57+
throw await createHttpError(response, this.baseUrl);
6558
}
6659

6760
if (!response.body) {
@@ -178,7 +171,9 @@ export class DeepSeekClient {
178171
if (isAbortError(error) && cancellationToken?.isCancellationRequested) {
179172
return;
180173
}
181-
callbacks.onError(error instanceof Error ? error : new Error(String(error)));
174+
const normalizedError = normalizeRequestError(error);
175+
logger.error('DeepSeek request failed:', getDiagnosticMessage(normalizedError), error);
176+
callbacks.onError(normalizedError);
182177
} finally {
183178
cancelListener?.dispose();
184179
}
@@ -188,3 +183,9 @@ export class DeepSeekClient {
188183
function isAbortError(error: unknown): boolean {
189184
return error instanceof Error && error.name === 'AbortError';
190185
}
186+
187+
function getDiagnosticMessage(error: Error): string {
188+
return 'diagnosticMessage' in error && typeof error.diagnosticMessage === 'string'
189+
? error.diagnosticMessage
190+
: error.message;
191+
}

0 commit comments

Comments
 (0)