@wlv-zedd/dsh-chatgpt-web 1.0.0 → 1.0.2
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/LICENSE +21 -21
- package/README.md +140 -140
- package/cordis.patch.yml +4 -4
- package/lib/cli.js +13269 -11860
- package/lib/plugin.js +3 -50
- package/package.json +89 -88
- package/src/adapters/base.ts +16 -16
- package/src/adapters/chatgpt-web/adapter-error.ts +59 -59
- package/src/adapters/chatgpt-web/browser-helper-main.ts +513 -513
- package/src/adapters/chatgpt-web/browser-helper-prompt-selection.ts +27 -27
- package/src/adapters/chatgpt-web/browser-worker.ts +4944 -4944
- package/src/adapters/chatgpt-web/codex-rollout-environment.ts +628 -628
- package/src/adapters/chatgpt-web/compaction-handoff.ts +533 -533
- package/src/adapters/chatgpt-web/compaction-transaction.ts +142 -142
- package/src/adapters/chatgpt-web/concurrency.ts +6 -6
- package/src/adapters/chatgpt-web/conversation-key.ts +58 -58
- package/src/adapters/chatgpt-web/environment.ts +669 -669
- package/src/adapters/chatgpt-web/index.ts +1544 -1544
- package/src/adapters/chatgpt-web/input-tokens.ts +74 -74
- package/src/adapters/chatgpt-web/launcher-helper-client.ts +695 -695
- package/src/adapters/chatgpt-web/markdown.ts +418 -418
- package/src/adapters/chatgpt-web/mcp-main.ts +25 -25
- package/src/adapters/chatgpt-web/mcp-server.ts +933 -933
- package/src/adapters/chatgpt-web/model.ts +70 -70
- package/src/adapters/chatgpt-web/native-compaction-control.ts +74 -74
- package/src/adapters/chatgpt-web/output-validation.ts +62 -62
- package/src/adapters/chatgpt-web/process-line-writer.ts +46 -46
- package/src/adapters/chatgpt-web/prompt.ts +702 -702
- package/src/adapters/chatgpt-web/retry-policy.ts +73 -73
- package/src/adapters/chatgpt-web/rolling-checkpoint.ts +384 -384
- package/src/adapters/chatgpt-web/thread-environment.ts +238 -238
- package/src/adapters/chatgpt-web/tool-stream-parser.ts +1 -1
- package/src/adapters/chatgpt-web/turn-broker.ts +1481 -1481
- package/src/adapters/chatgpt-web/turn-execution.ts +816 -816
- package/src/adapters/chatgpt-web/turn-progress.ts +292 -292
- package/src/adapters/chatgpt-web/usage.ts +121 -121
- package/src/adapters/image.ts +9 -9
- package/src/bridge.ts +1083 -1083
- package/src/browser-login.ts +521 -521
- package/src/chatgpt-session.ts +240 -240
- package/src/chatgpt-web-models.ts +400 -400
- package/src/cli.ts +568 -568
- package/src/codex-integration-document.ts +824 -824
- package/src/codex-integration-journal.ts +212 -212
- package/src/codex-integration-route.ts +515 -515
- package/src/codex-integration-shared.ts +332 -332
- package/src/codex-integration.ts +529 -529
- package/src/codex-interrupt-hook.ts +158 -158
- package/src/config.ts +616 -616
- package/src/dev-chat/cli.ts +432 -432
- package/src/dev-chat/constants.ts +3 -3
- package/src/dev-chat/driver.ts +655 -655
- package/src/dev-chat/profile.ts +223 -223
- package/src/dev-chat/session.ts +287 -287
- package/src/dev-chat/transport.ts +54 -54
- package/src/doctor.ts +237 -237
- package/src/event-queue.ts +45 -45
- package/src/http-body.ts +30 -30
- package/src/launcher-browser-host.ts +695 -695
- package/src/lib/errors.ts +281 -281
- package/src/lib/token-estimate.ts +42 -42
- package/src/login-helper.cjs +1 -1
- package/src/model-catalog.ts +197 -197
- package/src/native-passthrough.ts +261 -261
- package/src/plugin.ts +191 -191
- package/src/process.ts +45 -45
- package/src/responses/compaction.ts +199 -199
- package/src/responses/parser.ts +633 -633
- package/src/responses/reasoning-envelope.ts +49 -49
- package/src/responses/schema.ts +172 -172
- package/src/responses/state.ts +230 -230
- package/src/server.ts +1111 -1111
- package/src/service.ts +315 -315
- package/src/setup.ts +671 -671
- package/src/stall-timeout.ts +23 -23
- package/src/tunnel-service.ts +160 -160
- package/src/tunnel.ts +417 -417
- package/src/turndown-plugin-gfm.d.ts +5 -5
- package/src/types.ts +307 -307
- package/src/usage/totals.ts +12 -12
- package/src/version.ts +1 -1
package/src/lib/errors.ts
CHANGED
|
@@ -1,281 +1,281 @@
|
|
|
1
|
-
export interface CodexErrorPayload {
|
|
2
|
-
message: string;
|
|
3
|
-
type: string;
|
|
4
|
-
code: string | null;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
function isSubscriptionGateMessage(text: string): boolean {
|
|
8
|
-
return (
|
|
9
|
-
text.includes("requires a subscription") ||
|
|
10
|
-
text.includes("requires subscription") ||
|
|
11
|
-
text.includes("subscription required") ||
|
|
12
|
-
text.includes("upgrade for access") ||
|
|
13
|
-
text.includes("upgrade to pro") ||
|
|
14
|
-
text.includes("pro subscription") ||
|
|
15
|
-
(text.includes("upgrade") && text.includes("subscription"))
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function isAuthenticationMessage(text: string): boolean {
|
|
20
|
-
const accessDeniedWithCredentialCue = (
|
|
21
|
-
text.includes("access denied") ||
|
|
22
|
-
text.includes("accessdeniedexception")
|
|
23
|
-
) && (
|
|
24
|
-
text.includes("authentication") ||
|
|
25
|
-
text.includes("credential") ||
|
|
26
|
-
text.includes("api key") ||
|
|
27
|
-
text.includes("token") ||
|
|
28
|
-
text.includes("signature")
|
|
29
|
-
);
|
|
30
|
-
return (
|
|
31
|
-
text.includes("authentication failed") ||
|
|
32
|
-
text.includes("authentication") ||
|
|
33
|
-
text.includes("invalid_api_key") ||
|
|
34
|
-
text.includes("invalid api key") ||
|
|
35
|
-
text.includes("invalid token") ||
|
|
36
|
-
text.includes("unauthorizedexception") ||
|
|
37
|
-
text.includes("unrecognizedclientexception") ||
|
|
38
|
-
text.includes("unrecognizedclient") ||
|
|
39
|
-
text.includes("expired token") ||
|
|
40
|
-
text.includes("expiredtoken") ||
|
|
41
|
-
text.includes("unauthenticated") ||
|
|
42
|
-
text.includes("unauthorized") ||
|
|
43
|
-
accessDeniedWithCredentialCue
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function isPermissionMessage(text: string): boolean {
|
|
48
|
-
return (
|
|
49
|
-
text.includes("permission_denied") ||
|
|
50
|
-
text.includes("permission denied") ||
|
|
51
|
-
text.includes("forbidden") ||
|
|
52
|
-
text.includes("access denied") ||
|
|
53
|
-
text.includes("accessdeniedexception") ||
|
|
54
|
-
text.includes("not allowed to use") ||
|
|
55
|
-
text.includes("model access")
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Client cancelled / closed the turn. Matches only explicit client-abort phrases
|
|
61
|
-
* produced by request handlers and adapters. Deliberately narrow: bare "client closed"
|
|
62
|
-
* would also swallow legitimate upstream failures like "upstream HTTP client
|
|
63
|
-
* closed idle connection" and turn a real 502 into a 499.
|
|
64
|
-
*/
|
|
65
|
-
export function isClientClosedMessage(text: string): boolean {
|
|
66
|
-
const lower = text.toLowerCase();
|
|
67
|
-
return (
|
|
68
|
-
lower.includes("client closed request") ||
|
|
69
|
-
lower.includes("client cancelled request") ||
|
|
70
|
-
lower.includes("client canceled request") ||
|
|
71
|
-
lower.includes("request canceled by client") ||
|
|
72
|
-
lower.includes("request cancelled by client")
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function classifyError(status: number, type: string, message: string): CodexErrorPayload {
|
|
77
|
-
const text = message.toLowerCase();
|
|
78
|
-
// Preserve explicit cancel types; unify message-inferred client closes onto
|
|
79
|
-
// client_closed_request for /api/logs.
|
|
80
|
-
if (type === "client_cancelled") {
|
|
81
|
-
return { message, type: "client_cancelled", code: "client_cancelled" };
|
|
82
|
-
}
|
|
83
|
-
if (
|
|
84
|
-
status === 499 ||
|
|
85
|
-
type === "client_closed_request" ||
|
|
86
|
-
isClientClosedMessage(text)
|
|
87
|
-
) {
|
|
88
|
-
return { message, type: "invalid_request_error", code: "client_closed_request" };
|
|
89
|
-
}
|
|
90
|
-
if (
|
|
91
|
-
text.includes("context_length_exceeded") ||
|
|
92
|
-
text.includes("context window") ||
|
|
93
|
-
text.includes("context length") ||
|
|
94
|
-
text.includes("maximum context") ||
|
|
95
|
-
text.includes("too many tokens")
|
|
96
|
-
) {
|
|
97
|
-
return { message, type: "invalid_request_error", code: "context_length_exceeded" };
|
|
98
|
-
}
|
|
99
|
-
if (
|
|
100
|
-
text.includes("insufficient_quota") ||
|
|
101
|
-
text.includes("exceeded your current quota") ||
|
|
102
|
-
text.includes("quota exhausted") ||
|
|
103
|
-
text.includes("account quota exceeded") ||
|
|
104
|
-
text.includes("monthly quota exceeded") ||
|
|
105
|
-
text.includes("daily quota exceeded")
|
|
106
|
-
) {
|
|
107
|
-
return { message, type: "insufficient_quota", code: "insufficient_quota" };
|
|
108
|
-
}
|
|
109
|
-
if (
|
|
110
|
-
status === 429 ||
|
|
111
|
-
text.includes("rate limit") ||
|
|
112
|
-
text.includes("rate limited") ||
|
|
113
|
-
text.includes("too many requests") ||
|
|
114
|
-
text.includes("resource_exhausted") ||
|
|
115
|
-
text.includes("resource exhausted") ||
|
|
116
|
-
text.includes("throttlingexception") ||
|
|
117
|
-
text.includes("throttling")
|
|
118
|
-
) {
|
|
119
|
-
return { message, type: "rate_limit_error", code: "rate_limit_exceeded" };
|
|
120
|
-
}
|
|
121
|
-
if (type === "origin_rejected") {
|
|
122
|
-
return { message, type: "invalid_request_error", code: "origin_rejected" };
|
|
123
|
-
}
|
|
124
|
-
// HTTP 401 and explicit auth failures are authoritative even when provider text
|
|
125
|
-
// also advertises an upgrade or subscription.
|
|
126
|
-
if (
|
|
127
|
-
status === 401 ||
|
|
128
|
-
type === "authentication_error" ||
|
|
129
|
-
isAuthenticationMessage(text)
|
|
130
|
-
) {
|
|
131
|
-
return { message, type: "authentication_error", code: "invalid_api_key" };
|
|
132
|
-
}
|
|
133
|
-
// Subscription labels are valid only in a known permission context.
|
|
134
|
-
if (
|
|
135
|
-
(status === 403 || type === "permission_error") &&
|
|
136
|
-
isSubscriptionGateMessage(text)
|
|
137
|
-
) {
|
|
138
|
-
return { message, type: "permission_error", code: "subscription_required" };
|
|
139
|
-
}
|
|
140
|
-
if (
|
|
141
|
-
status === 403 ||
|
|
142
|
-
type === "permission_error" ||
|
|
143
|
-
isPermissionMessage(text)
|
|
144
|
-
) {
|
|
145
|
-
return { message, type: "permission_error", code: "permission_denied" };
|
|
146
|
-
}
|
|
147
|
-
if (
|
|
148
|
-
status === 503 ||
|
|
149
|
-
text.includes("overloaded") ||
|
|
150
|
-
text.includes("server is busy") ||
|
|
151
|
-
text.includes("temporarily unavailable")
|
|
152
|
-
) {
|
|
153
|
-
// Codex recognizes "server_is_overloaded" and applies retry-after backoff
|
|
154
|
-
// (responses.rs is_server_overloaded_error); generic "upstream_server_error" is not recognized.
|
|
155
|
-
return { message, type: "server_error", code: "server_is_overloaded" };
|
|
156
|
-
}
|
|
157
|
-
if (
|
|
158
|
-
text.includes("validationexception") ||
|
|
159
|
-
text.includes("invalid request") ||
|
|
160
|
-
text.includes("model unavailable") ||
|
|
161
|
-
text.includes("model not found") ||
|
|
162
|
-
text.includes("unsupported model")
|
|
163
|
-
) {
|
|
164
|
-
return { message, type: "invalid_request_error", code: "invalid_request_error" };
|
|
165
|
-
}
|
|
166
|
-
if (status >= 500) {
|
|
167
|
-
return { message, type: "server_error", code: "upstream_server_error" };
|
|
168
|
-
}
|
|
169
|
-
if (status === 400 || type === "invalid_request_error") {
|
|
170
|
-
return { message, type: "invalid_request_error", code: "invalid_request_error" };
|
|
171
|
-
}
|
|
172
|
-
return { message, type, code: type || null };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Best-effort parse of a retry delay embedded in an upstream error message. */
|
|
176
|
-
export function parseRetryAfterFromMessage(message: string): number | undefined {
|
|
177
|
-
const patterns = [
|
|
178
|
-
/try again in (\d+(?:\.\d+)?)\s*s(?:ec(?:ond)?s?)?/i,
|
|
179
|
-
/retry after (\d+(?:\.\d+)?)\s*s(?:ec(?:ond)?s?)?/i,
|
|
180
|
-
/retry[- ]after[:\s]+(\d+)/i,
|
|
181
|
-
];
|
|
182
|
-
for (const pattern of patterns) {
|
|
183
|
-
const match = message.match(pattern);
|
|
184
|
-
if (!match?.[1]) continue;
|
|
185
|
-
const seconds = Number.parseFloat(match[1]);
|
|
186
|
-
if (Number.isFinite(seconds) && seconds > 0) return Math.ceil(seconds);
|
|
187
|
-
}
|
|
188
|
-
return undefined;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/** Infer HTTP status from adapter terminal error text (provider-agnostic keyword matching). */
|
|
192
|
-
export function inferHttpStatusFromAdapterMessage(message: string): number {
|
|
193
|
-
const lower = message.toLowerCase();
|
|
194
|
-
// Client aborts must not look like upstream 502s in /api/logs.
|
|
195
|
-
if (isClientClosedMessage(lower)) return 499;
|
|
196
|
-
if (
|
|
197
|
-
lower.includes("resource_exhausted") ||
|
|
198
|
-
lower.includes("resource exhausted") ||
|
|
199
|
-
lower.includes("rate limit") ||
|
|
200
|
-
lower.includes("too many requests") ||
|
|
201
|
-
lower.includes("throttling")
|
|
202
|
-
) return 429;
|
|
203
|
-
// Strong authentication signals win when a message contains mixed auth and
|
|
204
|
-
// subscription/permission wording.
|
|
205
|
-
if (isAuthenticationMessage(lower)) return 401;
|
|
206
|
-
if (isSubscriptionGateMessage(lower) || isPermissionMessage(lower)) return 403;
|
|
207
|
-
if (
|
|
208
|
-
lower.includes("unavailable") ||
|
|
209
|
-
lower.includes("overloaded") ||
|
|
210
|
-
lower.includes("temporarily") ||
|
|
211
|
-
lower.includes("server is busy")
|
|
212
|
-
) return 503;
|
|
213
|
-
if (
|
|
214
|
-
lower.includes("invalid") ||
|
|
215
|
-
lower.includes("not found") ||
|
|
216
|
-
lower.includes("unsupported") ||
|
|
217
|
-
lower.includes("malformed") ||
|
|
218
|
-
lower.includes("unimplemented")
|
|
219
|
-
) return 400;
|
|
220
|
-
if (
|
|
221
|
-
lower.includes("timed out") ||
|
|
222
|
-
lower.includes("timeout") ||
|
|
223
|
-
lower.includes("etimedout") ||
|
|
224
|
-
lower.includes("deadline")
|
|
225
|
-
) return 504;
|
|
226
|
-
return 502;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/** Map an adapter terminal error message to HTTP status + classified Codex error payload. */
|
|
230
|
-
export function adapterFailureFromMessage(message: string): { httpStatus: number; error: CodexErrorPayload } {
|
|
231
|
-
const httpStatus = inferHttpStatusFromAdapterMessage(message);
|
|
232
|
-
let finalMessage = message;
|
|
233
|
-
const retryAfterSeconds = parseRetryAfterFromMessage(message);
|
|
234
|
-
if (retryAfterSeconds && !/please try again in /i.test(message)) {
|
|
235
|
-
finalMessage = `${message} Please try again in ${retryAfterSeconds}s.`;
|
|
236
|
-
}
|
|
237
|
-
const errorType = httpStatus === 499
|
|
238
|
-
? "client_closed_request"
|
|
239
|
-
: httpStatus === 429
|
|
240
|
-
? "rate_limit_error"
|
|
241
|
-
: httpStatus === 401
|
|
242
|
-
? "authentication_error"
|
|
243
|
-
: httpStatus === 403
|
|
244
|
-
? "permission_error"
|
|
245
|
-
: httpStatus === 503 || httpStatus === 504
|
|
246
|
-
? "server_error"
|
|
247
|
-
: httpStatus === 400
|
|
248
|
-
? "invalid_request_error"
|
|
249
|
-
: "upstream_error";
|
|
250
|
-
return {
|
|
251
|
-
httpStatus,
|
|
252
|
-
error: classifyError(httpStatus, errorType, finalMessage),
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/** Map a terminal Responses error object to the HTTP status we record in /api/logs. */
|
|
257
|
-
export function httpStatusFromTerminalError(error: {
|
|
258
|
-
type?: string;
|
|
259
|
-
code?: string | null;
|
|
260
|
-
message?: string;
|
|
261
|
-
} | undefined): number {
|
|
262
|
-
if (!error) return 502;
|
|
263
|
-
if (error.code === "client_closed_request" || error.code === "client_cancelled") return 499;
|
|
264
|
-
if (error.type === "rate_limit_error" || error.code === "rate_limit_exceeded") return 429;
|
|
265
|
-
if (error.type === "authentication_error" || error.code === "invalid_api_key") return 401;
|
|
266
|
-
if (
|
|
267
|
-
error.type === "permission_error" ||
|
|
268
|
-
error.code === "permission_denied" ||
|
|
269
|
-
error.code === "subscription_required"
|
|
270
|
-
) return 403;
|
|
271
|
-
if (error.type === "insufficient_quota" || error.code === "insufficient_quota") return 429;
|
|
272
|
-
if (error.type === "server_error" && error.code === "server_is_overloaded") return 503;
|
|
273
|
-
// Client-closed messages often arrive as invalid_request_error after classifyError; check message
|
|
274
|
-
// before treating every invalid_request_error as HTTP 400.
|
|
275
|
-
const message = error.message ?? "";
|
|
276
|
-
if (message && isClientClosedMessage(message)) return 499;
|
|
277
|
-
if (error.type === "invalid_request_error") return 400;
|
|
278
|
-
if (error.type === "proxy_error") return 500;
|
|
279
|
-
if (message) return inferHttpStatusFromAdapterMessage(message);
|
|
280
|
-
return 502;
|
|
281
|
-
}
|
|
1
|
+
export interface CodexErrorPayload {
|
|
2
|
+
message: string;
|
|
3
|
+
type: string;
|
|
4
|
+
code: string | null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function isSubscriptionGateMessage(text: string): boolean {
|
|
8
|
+
return (
|
|
9
|
+
text.includes("requires a subscription") ||
|
|
10
|
+
text.includes("requires subscription") ||
|
|
11
|
+
text.includes("subscription required") ||
|
|
12
|
+
text.includes("upgrade for access") ||
|
|
13
|
+
text.includes("upgrade to pro") ||
|
|
14
|
+
text.includes("pro subscription") ||
|
|
15
|
+
(text.includes("upgrade") && text.includes("subscription"))
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isAuthenticationMessage(text: string): boolean {
|
|
20
|
+
const accessDeniedWithCredentialCue = (
|
|
21
|
+
text.includes("access denied") ||
|
|
22
|
+
text.includes("accessdeniedexception")
|
|
23
|
+
) && (
|
|
24
|
+
text.includes("authentication") ||
|
|
25
|
+
text.includes("credential") ||
|
|
26
|
+
text.includes("api key") ||
|
|
27
|
+
text.includes("token") ||
|
|
28
|
+
text.includes("signature")
|
|
29
|
+
);
|
|
30
|
+
return (
|
|
31
|
+
text.includes("authentication failed") ||
|
|
32
|
+
text.includes("authentication") ||
|
|
33
|
+
text.includes("invalid_api_key") ||
|
|
34
|
+
text.includes("invalid api key") ||
|
|
35
|
+
text.includes("invalid token") ||
|
|
36
|
+
text.includes("unauthorizedexception") ||
|
|
37
|
+
text.includes("unrecognizedclientexception") ||
|
|
38
|
+
text.includes("unrecognizedclient") ||
|
|
39
|
+
text.includes("expired token") ||
|
|
40
|
+
text.includes("expiredtoken") ||
|
|
41
|
+
text.includes("unauthenticated") ||
|
|
42
|
+
text.includes("unauthorized") ||
|
|
43
|
+
accessDeniedWithCredentialCue
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isPermissionMessage(text: string): boolean {
|
|
48
|
+
return (
|
|
49
|
+
text.includes("permission_denied") ||
|
|
50
|
+
text.includes("permission denied") ||
|
|
51
|
+
text.includes("forbidden") ||
|
|
52
|
+
text.includes("access denied") ||
|
|
53
|
+
text.includes("accessdeniedexception") ||
|
|
54
|
+
text.includes("not allowed to use") ||
|
|
55
|
+
text.includes("model access")
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Client cancelled / closed the turn. Matches only explicit client-abort phrases
|
|
61
|
+
* produced by request handlers and adapters. Deliberately narrow: bare "client closed"
|
|
62
|
+
* would also swallow legitimate upstream failures like "upstream HTTP client
|
|
63
|
+
* closed idle connection" and turn a real 502 into a 499.
|
|
64
|
+
*/
|
|
65
|
+
export function isClientClosedMessage(text: string): boolean {
|
|
66
|
+
const lower = text.toLowerCase();
|
|
67
|
+
return (
|
|
68
|
+
lower.includes("client closed request") ||
|
|
69
|
+
lower.includes("client cancelled request") ||
|
|
70
|
+
lower.includes("client canceled request") ||
|
|
71
|
+
lower.includes("request canceled by client") ||
|
|
72
|
+
lower.includes("request cancelled by client")
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function classifyError(status: number, type: string, message: string): CodexErrorPayload {
|
|
77
|
+
const text = message.toLowerCase();
|
|
78
|
+
// Preserve explicit cancel types; unify message-inferred client closes onto
|
|
79
|
+
// client_closed_request for /api/logs.
|
|
80
|
+
if (type === "client_cancelled") {
|
|
81
|
+
return { message, type: "client_cancelled", code: "client_cancelled" };
|
|
82
|
+
}
|
|
83
|
+
if (
|
|
84
|
+
status === 499 ||
|
|
85
|
+
type === "client_closed_request" ||
|
|
86
|
+
isClientClosedMessage(text)
|
|
87
|
+
) {
|
|
88
|
+
return { message, type: "invalid_request_error", code: "client_closed_request" };
|
|
89
|
+
}
|
|
90
|
+
if (
|
|
91
|
+
text.includes("context_length_exceeded") ||
|
|
92
|
+
text.includes("context window") ||
|
|
93
|
+
text.includes("context length") ||
|
|
94
|
+
text.includes("maximum context") ||
|
|
95
|
+
text.includes("too many tokens")
|
|
96
|
+
) {
|
|
97
|
+
return { message, type: "invalid_request_error", code: "context_length_exceeded" };
|
|
98
|
+
}
|
|
99
|
+
if (
|
|
100
|
+
text.includes("insufficient_quota") ||
|
|
101
|
+
text.includes("exceeded your current quota") ||
|
|
102
|
+
text.includes("quota exhausted") ||
|
|
103
|
+
text.includes("account quota exceeded") ||
|
|
104
|
+
text.includes("monthly quota exceeded") ||
|
|
105
|
+
text.includes("daily quota exceeded")
|
|
106
|
+
) {
|
|
107
|
+
return { message, type: "insufficient_quota", code: "insufficient_quota" };
|
|
108
|
+
}
|
|
109
|
+
if (
|
|
110
|
+
status === 429 ||
|
|
111
|
+
text.includes("rate limit") ||
|
|
112
|
+
text.includes("rate limited") ||
|
|
113
|
+
text.includes("too many requests") ||
|
|
114
|
+
text.includes("resource_exhausted") ||
|
|
115
|
+
text.includes("resource exhausted") ||
|
|
116
|
+
text.includes("throttlingexception") ||
|
|
117
|
+
text.includes("throttling")
|
|
118
|
+
) {
|
|
119
|
+
return { message, type: "rate_limit_error", code: "rate_limit_exceeded" };
|
|
120
|
+
}
|
|
121
|
+
if (type === "origin_rejected") {
|
|
122
|
+
return { message, type: "invalid_request_error", code: "origin_rejected" };
|
|
123
|
+
}
|
|
124
|
+
// HTTP 401 and explicit auth failures are authoritative even when provider text
|
|
125
|
+
// also advertises an upgrade or subscription.
|
|
126
|
+
if (
|
|
127
|
+
status === 401 ||
|
|
128
|
+
type === "authentication_error" ||
|
|
129
|
+
isAuthenticationMessage(text)
|
|
130
|
+
) {
|
|
131
|
+
return { message, type: "authentication_error", code: "invalid_api_key" };
|
|
132
|
+
}
|
|
133
|
+
// Subscription labels are valid only in a known permission context.
|
|
134
|
+
if (
|
|
135
|
+
(status === 403 || type === "permission_error") &&
|
|
136
|
+
isSubscriptionGateMessage(text)
|
|
137
|
+
) {
|
|
138
|
+
return { message, type: "permission_error", code: "subscription_required" };
|
|
139
|
+
}
|
|
140
|
+
if (
|
|
141
|
+
status === 403 ||
|
|
142
|
+
type === "permission_error" ||
|
|
143
|
+
isPermissionMessage(text)
|
|
144
|
+
) {
|
|
145
|
+
return { message, type: "permission_error", code: "permission_denied" };
|
|
146
|
+
}
|
|
147
|
+
if (
|
|
148
|
+
status === 503 ||
|
|
149
|
+
text.includes("overloaded") ||
|
|
150
|
+
text.includes("server is busy") ||
|
|
151
|
+
text.includes("temporarily unavailable")
|
|
152
|
+
) {
|
|
153
|
+
// Codex recognizes "server_is_overloaded" and applies retry-after backoff
|
|
154
|
+
// (responses.rs is_server_overloaded_error); generic "upstream_server_error" is not recognized.
|
|
155
|
+
return { message, type: "server_error", code: "server_is_overloaded" };
|
|
156
|
+
}
|
|
157
|
+
if (
|
|
158
|
+
text.includes("validationexception") ||
|
|
159
|
+
text.includes("invalid request") ||
|
|
160
|
+
text.includes("model unavailable") ||
|
|
161
|
+
text.includes("model not found") ||
|
|
162
|
+
text.includes("unsupported model")
|
|
163
|
+
) {
|
|
164
|
+
return { message, type: "invalid_request_error", code: "invalid_request_error" };
|
|
165
|
+
}
|
|
166
|
+
if (status >= 500) {
|
|
167
|
+
return { message, type: "server_error", code: "upstream_server_error" };
|
|
168
|
+
}
|
|
169
|
+
if (status === 400 || type === "invalid_request_error") {
|
|
170
|
+
return { message, type: "invalid_request_error", code: "invalid_request_error" };
|
|
171
|
+
}
|
|
172
|
+
return { message, type, code: type || null };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Best-effort parse of a retry delay embedded in an upstream error message. */
|
|
176
|
+
export function parseRetryAfterFromMessage(message: string): number | undefined {
|
|
177
|
+
const patterns = [
|
|
178
|
+
/try again in (\d+(?:\.\d+)?)\s*s(?:ec(?:ond)?s?)?/i,
|
|
179
|
+
/retry after (\d+(?:\.\d+)?)\s*s(?:ec(?:ond)?s?)?/i,
|
|
180
|
+
/retry[- ]after[:\s]+(\d+)/i,
|
|
181
|
+
];
|
|
182
|
+
for (const pattern of patterns) {
|
|
183
|
+
const match = message.match(pattern);
|
|
184
|
+
if (!match?.[1]) continue;
|
|
185
|
+
const seconds = Number.parseFloat(match[1]);
|
|
186
|
+
if (Number.isFinite(seconds) && seconds > 0) return Math.ceil(seconds);
|
|
187
|
+
}
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Infer HTTP status from adapter terminal error text (provider-agnostic keyword matching). */
|
|
192
|
+
export function inferHttpStatusFromAdapterMessage(message: string): number {
|
|
193
|
+
const lower = message.toLowerCase();
|
|
194
|
+
// Client aborts must not look like upstream 502s in /api/logs.
|
|
195
|
+
if (isClientClosedMessage(lower)) return 499;
|
|
196
|
+
if (
|
|
197
|
+
lower.includes("resource_exhausted") ||
|
|
198
|
+
lower.includes("resource exhausted") ||
|
|
199
|
+
lower.includes("rate limit") ||
|
|
200
|
+
lower.includes("too many requests") ||
|
|
201
|
+
lower.includes("throttling")
|
|
202
|
+
) return 429;
|
|
203
|
+
// Strong authentication signals win when a message contains mixed auth and
|
|
204
|
+
// subscription/permission wording.
|
|
205
|
+
if (isAuthenticationMessage(lower)) return 401;
|
|
206
|
+
if (isSubscriptionGateMessage(lower) || isPermissionMessage(lower)) return 403;
|
|
207
|
+
if (
|
|
208
|
+
lower.includes("unavailable") ||
|
|
209
|
+
lower.includes("overloaded") ||
|
|
210
|
+
lower.includes("temporarily") ||
|
|
211
|
+
lower.includes("server is busy")
|
|
212
|
+
) return 503;
|
|
213
|
+
if (
|
|
214
|
+
lower.includes("invalid") ||
|
|
215
|
+
lower.includes("not found") ||
|
|
216
|
+
lower.includes("unsupported") ||
|
|
217
|
+
lower.includes("malformed") ||
|
|
218
|
+
lower.includes("unimplemented")
|
|
219
|
+
) return 400;
|
|
220
|
+
if (
|
|
221
|
+
lower.includes("timed out") ||
|
|
222
|
+
lower.includes("timeout") ||
|
|
223
|
+
lower.includes("etimedout") ||
|
|
224
|
+
lower.includes("deadline")
|
|
225
|
+
) return 504;
|
|
226
|
+
return 502;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Map an adapter terminal error message to HTTP status + classified Codex error payload. */
|
|
230
|
+
export function adapterFailureFromMessage(message: string): { httpStatus: number; error: CodexErrorPayload } {
|
|
231
|
+
const httpStatus = inferHttpStatusFromAdapterMessage(message);
|
|
232
|
+
let finalMessage = message;
|
|
233
|
+
const retryAfterSeconds = parseRetryAfterFromMessage(message);
|
|
234
|
+
if (retryAfterSeconds && !/please try again in /i.test(message)) {
|
|
235
|
+
finalMessage = `${message} Please try again in ${retryAfterSeconds}s.`;
|
|
236
|
+
}
|
|
237
|
+
const errorType = httpStatus === 499
|
|
238
|
+
? "client_closed_request"
|
|
239
|
+
: httpStatus === 429
|
|
240
|
+
? "rate_limit_error"
|
|
241
|
+
: httpStatus === 401
|
|
242
|
+
? "authentication_error"
|
|
243
|
+
: httpStatus === 403
|
|
244
|
+
? "permission_error"
|
|
245
|
+
: httpStatus === 503 || httpStatus === 504
|
|
246
|
+
? "server_error"
|
|
247
|
+
: httpStatus === 400
|
|
248
|
+
? "invalid_request_error"
|
|
249
|
+
: "upstream_error";
|
|
250
|
+
return {
|
|
251
|
+
httpStatus,
|
|
252
|
+
error: classifyError(httpStatus, errorType, finalMessage),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Map a terminal Responses error object to the HTTP status we record in /api/logs. */
|
|
257
|
+
export function httpStatusFromTerminalError(error: {
|
|
258
|
+
type?: string;
|
|
259
|
+
code?: string | null;
|
|
260
|
+
message?: string;
|
|
261
|
+
} | undefined): number {
|
|
262
|
+
if (!error) return 502;
|
|
263
|
+
if (error.code === "client_closed_request" || error.code === "client_cancelled") return 499;
|
|
264
|
+
if (error.type === "rate_limit_error" || error.code === "rate_limit_exceeded") return 429;
|
|
265
|
+
if (error.type === "authentication_error" || error.code === "invalid_api_key") return 401;
|
|
266
|
+
if (
|
|
267
|
+
error.type === "permission_error" ||
|
|
268
|
+
error.code === "permission_denied" ||
|
|
269
|
+
error.code === "subscription_required"
|
|
270
|
+
) return 403;
|
|
271
|
+
if (error.type === "insufficient_quota" || error.code === "insufficient_quota") return 429;
|
|
272
|
+
if (error.type === "server_error" && error.code === "server_is_overloaded") return 503;
|
|
273
|
+
// Client-closed messages often arrive as invalid_request_error after classifyError; check message
|
|
274
|
+
// before treating every invalid_request_error as HTTP 400.
|
|
275
|
+
const message = error.message ?? "";
|
|
276
|
+
if (message && isClientClosedMessage(message)) return 499;
|
|
277
|
+
if (error.type === "invalid_request_error") return 400;
|
|
278
|
+
if (error.type === "proxy_error") return 500;
|
|
279
|
+
if (message) return inferHttpStatusFromAdapterMessage(message);
|
|
280
|
+
return 502;
|
|
281
|
+
}
|
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
import { get_encoding, type Tiktoken } from "tiktoken";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Token accounting for ChatGPT Web prompts.
|
|
5
|
-
*
|
|
6
|
-
* A character ratio is not safe here: dense JSON/base64 can contain far more tokens than prose
|
|
7
|
-
* of the same length. Count with the tokenizer used by the GPT-5 generation instead.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
const TOKENIZER_CHUNK_CHARS = 4_096;
|
|
11
|
-
let tokenizer: Tiktoken | undefined;
|
|
12
|
-
|
|
13
|
-
function chatGptTokenizer(): Tiktoken {
|
|
14
|
-
tokenizer ??= get_encoding("o200k_base");
|
|
15
|
-
return tokenizer;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Count ordinary text conservatively without handing pathological multi-megabyte runs to one
|
|
20
|
-
* tokenizer call. Independent chunks can only lose cross-boundary merges, so their sum may
|
|
21
|
-
* over-count slightly but cannot under-count because of a missed boundary token.
|
|
22
|
-
*/
|
|
23
|
-
export function estimateTokens(text: string, modelId?: string): number {
|
|
24
|
-
void modelId;
|
|
25
|
-
if (!text) return 0;
|
|
26
|
-
|
|
27
|
-
const encoding = chatGptTokenizer();
|
|
28
|
-
let count = 0;
|
|
29
|
-
for (let start = 0; start < text.length;) {
|
|
30
|
-
let end = Math.min(start + TOKENIZER_CHUNK_CHARS, text.length);
|
|
31
|
-
if (end < text.length) {
|
|
32
|
-
const previous = text.charCodeAt(end - 1);
|
|
33
|
-
const next = text.charCodeAt(end);
|
|
34
|
-
if (previous >= 0xD800 && previous <= 0xDBFF && next >= 0xDC00 && next <= 0xDFFF) {
|
|
35
|
-
end -= 1;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
count += encoding.encode_ordinary(text.slice(start, end)).length;
|
|
39
|
-
start = end;
|
|
40
|
-
}
|
|
41
|
-
return count;
|
|
42
|
-
}
|
|
1
|
+
import { get_encoding, type Tiktoken } from "tiktoken";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Token accounting for ChatGPT Web prompts.
|
|
5
|
+
*
|
|
6
|
+
* A character ratio is not safe here: dense JSON/base64 can contain far more tokens than prose
|
|
7
|
+
* of the same length. Count with the tokenizer used by the GPT-5 generation instead.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const TOKENIZER_CHUNK_CHARS = 4_096;
|
|
11
|
+
let tokenizer: Tiktoken | undefined;
|
|
12
|
+
|
|
13
|
+
function chatGptTokenizer(): Tiktoken {
|
|
14
|
+
tokenizer ??= get_encoding("o200k_base");
|
|
15
|
+
return tokenizer;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Count ordinary text conservatively without handing pathological multi-megabyte runs to one
|
|
20
|
+
* tokenizer call. Independent chunks can only lose cross-boundary merges, so their sum may
|
|
21
|
+
* over-count slightly but cannot under-count because of a missed boundary token.
|
|
22
|
+
*/
|
|
23
|
+
export function estimateTokens(text: string, modelId?: string): number {
|
|
24
|
+
void modelId;
|
|
25
|
+
if (!text) return 0;
|
|
26
|
+
|
|
27
|
+
const encoding = chatGptTokenizer();
|
|
28
|
+
let count = 0;
|
|
29
|
+
for (let start = 0; start < text.length;) {
|
|
30
|
+
let end = Math.min(start + TOKENIZER_CHUNK_CHARS, text.length);
|
|
31
|
+
if (end < text.length) {
|
|
32
|
+
const previous = text.charCodeAt(end - 1);
|
|
33
|
+
const next = text.charCodeAt(end);
|
|
34
|
+
if (previous >= 0xD800 && previous <= 0xDBFF && next >= 0xDC00 && next <= 0xDFFF) {
|
|
35
|
+
end -= 1;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
count += encoding.encode_ordinary(text.slice(start, end)).length;
|
|
39
|
+
start = end;
|
|
40
|
+
}
|
|
41
|
+
return count;
|
|
42
|
+
}
|
package/src/login-helper.cjs
CHANGED