Repository navigation
Expand file tree
/
Copy pathfailure-reason.ts
More file actions
94 lines (85 loc) · 3.67 KB
/
Copy pathfailure-reason.ts
File metadata and controls
94 lines (85 loc) · 3.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import {
AIError,
AIResponseError,
AIServerError,
AIUnknownError,
browserIsOffline,
isAIAuthError,
isAIModelRestrictionError,
isAINetworkError,
isAIProviderError,
isAIRateLimitError,
isAISubscriptionError,
isBrowserNetworkFailure,
isTimeoutError,
} from './errors';
/** Why an AI request failed, as one value from a closed set that is safe to send to telemetry. */
export type AIFailureCode =
| 'limit_reached'
| 'model_restricted'
| 'auth_expired'
| 'timeout'
| 'network'
| 'aborted'
| 'provider_error'
| 'invalid_response'
| 'server_error'
| 'unknown';
/** Where the request was when it failed: sending it, reading a streamed answer, or checking the answer. */
export type AIFailureStage = 'request' | 'stream' | 'parse';
/** What the student can do next. */
export type AIFailureRecovery = 'plan' | 'upgrade' | 'sign_in' | 'retry' | 'none';
// A DOMException is not an Error in every runtime, so check the name.
function isAbort(error: unknown): boolean {
return typeof error === 'object' && error !== null && (error as { name?: unknown }).name === 'AbortError';
}
function isResponseError(error: unknown): boolean {
return error instanceof AIResponseError || (error instanceof Error && error.name === 'AIResponseError');
}
export function aiFailureCode(error: unknown, userInitiatedAbort = false): AIFailureCode {
if (userInitiatedAbort || isAbort(error)) return 'aborted';
// A 429 is the account's request rate, which is a limit the student hit too.
if (isAISubscriptionError(error) || isAIRateLimitError(error)) return 'limit_reached';
if (isAIModelRestrictionError(error)) return 'model_restricted';
if (isAIAuthError(error)) return 'auth_expired';
if (isTimeoutError(error)) return 'timeout';
if (isResponseError(error)) return 'invalid_response';
if (isAIProviderError(error)) return 'provider_error';
if (isAINetworkError(error) || isBrowserNetworkFailure(error)) return 'network';
if (error instanceof AIServerError || (error instanceof Error && error.name === 'AIServerError')) return 'server_error';
if (error instanceof AIUnknownError) return 'unknown';
if (!(error instanceof AIError) && browserIsOffline()) return 'network';
if (error instanceof AIError && typeof error.statusCode === 'number' && error.statusCode >= 500) return 'server_error';
return 'unknown';
}
/** The stage a caller saw, except that a rejected answer is always the parse stage. */
export function aiFailureStage(error: unknown, stage: AIFailureStage = 'request'): AIFailureStage {
return isResponseError(error) ? 'parse' : stage;
}
/**
* The HTTP status that goes with a failure, when one is known and plausible.
* A TimeoutError's 408 is the browser's own deadline, not a server answer.
*/
export function aiFailureHttpStatus(error: unknown): number | undefined {
if (isTimeoutError(error)) return undefined;
const status = error instanceof AIError ? error.statusCode : undefined;
return Number.isInteger(status) && status! >= 100 && status! <= 599 ? status : undefined;
}
/**
* The next step a failure offers. Limits open the plan dialog and a Pro model
* the upgrade dialog, which every tool already mounts. A refused session needs
* a fresh sign-in, because sending again would be refused the same way. A
* request that is too large fails the same way on every try.
*/
export function aiFailureRecovery(error: unknown): AIFailureRecovery {
if (isAISubscriptionError(error)) return 'plan';
if (isAIModelRestrictionError(error)) return 'upgrade';
switch (aiFailureCode(error)) {
case 'auth_expired':
return 'sign_in';
case 'aborted':
return 'none';
default:
return aiFailureHttpStatus(error) === 413 ? 'none' : 'retry';
}
}