@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.
Files changed (81) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +140 -140
  3. package/cordis.patch.yml +4 -4
  4. package/lib/cli.js +13269 -11860
  5. package/lib/plugin.js +3 -50
  6. package/package.json +89 -88
  7. package/src/adapters/base.ts +16 -16
  8. package/src/adapters/chatgpt-web/adapter-error.ts +59 -59
  9. package/src/adapters/chatgpt-web/browser-helper-main.ts +513 -513
  10. package/src/adapters/chatgpt-web/browser-helper-prompt-selection.ts +27 -27
  11. package/src/adapters/chatgpt-web/browser-worker.ts +4944 -4944
  12. package/src/adapters/chatgpt-web/codex-rollout-environment.ts +628 -628
  13. package/src/adapters/chatgpt-web/compaction-handoff.ts +533 -533
  14. package/src/adapters/chatgpt-web/compaction-transaction.ts +142 -142
  15. package/src/adapters/chatgpt-web/concurrency.ts +6 -6
  16. package/src/adapters/chatgpt-web/conversation-key.ts +58 -58
  17. package/src/adapters/chatgpt-web/environment.ts +669 -669
  18. package/src/adapters/chatgpt-web/index.ts +1544 -1544
  19. package/src/adapters/chatgpt-web/input-tokens.ts +74 -74
  20. package/src/adapters/chatgpt-web/launcher-helper-client.ts +695 -695
  21. package/src/adapters/chatgpt-web/markdown.ts +418 -418
  22. package/src/adapters/chatgpt-web/mcp-main.ts +25 -25
  23. package/src/adapters/chatgpt-web/mcp-server.ts +933 -933
  24. package/src/adapters/chatgpt-web/model.ts +70 -70
  25. package/src/adapters/chatgpt-web/native-compaction-control.ts +74 -74
  26. package/src/adapters/chatgpt-web/output-validation.ts +62 -62
  27. package/src/adapters/chatgpt-web/process-line-writer.ts +46 -46
  28. package/src/adapters/chatgpt-web/prompt.ts +702 -702
  29. package/src/adapters/chatgpt-web/retry-policy.ts +73 -73
  30. package/src/adapters/chatgpt-web/rolling-checkpoint.ts +384 -384
  31. package/src/adapters/chatgpt-web/thread-environment.ts +238 -238
  32. package/src/adapters/chatgpt-web/tool-stream-parser.ts +1 -1
  33. package/src/adapters/chatgpt-web/turn-broker.ts +1481 -1481
  34. package/src/adapters/chatgpt-web/turn-execution.ts +816 -816
  35. package/src/adapters/chatgpt-web/turn-progress.ts +292 -292
  36. package/src/adapters/chatgpt-web/usage.ts +121 -121
  37. package/src/adapters/image.ts +9 -9
  38. package/src/bridge.ts +1083 -1083
  39. package/src/browser-login.ts +521 -521
  40. package/src/chatgpt-session.ts +240 -240
  41. package/src/chatgpt-web-models.ts +400 -400
  42. package/src/cli.ts +568 -568
  43. package/src/codex-integration-document.ts +824 -824
  44. package/src/codex-integration-journal.ts +212 -212
  45. package/src/codex-integration-route.ts +515 -515
  46. package/src/codex-integration-shared.ts +332 -332
  47. package/src/codex-integration.ts +529 -529
  48. package/src/codex-interrupt-hook.ts +158 -158
  49. package/src/config.ts +616 -616
  50. package/src/dev-chat/cli.ts +432 -432
  51. package/src/dev-chat/constants.ts +3 -3
  52. package/src/dev-chat/driver.ts +655 -655
  53. package/src/dev-chat/profile.ts +223 -223
  54. package/src/dev-chat/session.ts +287 -287
  55. package/src/dev-chat/transport.ts +54 -54
  56. package/src/doctor.ts +237 -237
  57. package/src/event-queue.ts +45 -45
  58. package/src/http-body.ts +30 -30
  59. package/src/launcher-browser-host.ts +695 -695
  60. package/src/lib/errors.ts +281 -281
  61. package/src/lib/token-estimate.ts +42 -42
  62. package/src/login-helper.cjs +1 -1
  63. package/src/model-catalog.ts +197 -197
  64. package/src/native-passthrough.ts +261 -261
  65. package/src/plugin.ts +191 -191
  66. package/src/process.ts +45 -45
  67. package/src/responses/compaction.ts +199 -199
  68. package/src/responses/parser.ts +633 -633
  69. package/src/responses/reasoning-envelope.ts +49 -49
  70. package/src/responses/schema.ts +172 -172
  71. package/src/responses/state.ts +230 -230
  72. package/src/server.ts +1111 -1111
  73. package/src/service.ts +315 -315
  74. package/src/setup.ts +671 -671
  75. package/src/stall-timeout.ts +23 -23
  76. package/src/tunnel-service.ts +160 -160
  77. package/src/tunnel.ts +417 -417
  78. package/src/turndown-plugin-gfm.d.ts +5 -5
  79. package/src/types.ts +307 -307
  80. package/src/usage/totals.ts +12 -12
  81. 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
+ }
@@ -137,4 +137,4 @@ async function main() {
137
137
  main().catch(err => {
138
138
  process.stderr.write(`login-helper: ${err.message}\n`);
139
139
  process.exit(1);
140
- });
140
+ });