dsh-codex-community 0.0.1
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/CHANGELOG.md +90 -0
- package/CONTRIBUTING.en.md +54 -0
- package/CONTRIBUTING.md +54 -0
- package/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.en.md +98 -0
- package/README.md +98 -0
- package/SECURITY.md +41 -0
- package/SUPPORT.md +17 -0
- package/THIRD_PARTY_NOTICES.md +25 -0
- package/codex-community.patch.yml +13 -0
- package/dist/client/index.js +1037 -0
- package/dist/host/index.mjs +98 -0
- package/dist/internal/authorization-bridge.mjs +662 -0
- package/dist/internal/authorization-commit-tracker.mjs +49 -0
- package/dist/internal/codex-authorization.mjs +202 -0
- package/dist/internal/codex-credential-store.mjs +164 -0
- package/dist/internal/codex-identifiers.mjs +4 -0
- package/dist/internal/codex-pi-provider.mjs +137 -0
- package/dist/internal/codex-provider-runtime.mjs +256 -0
- package/dist/internal/codex-route-adapter.mjs +133 -0
- package/dist/internal/codex-session-resources.mjs +64 -0
- package/dist/internal/failure-normalizer.mjs +456 -0
- package/dist/internal/image-policy.mjs +45 -0
- package/dist/internal/quota-observer.mjs +142 -0
- package/dist/internal/reliability.mjs +12 -0
- package/dist/internal/remote-image-input.mjs +801 -0
- package/dist/internal/session-preference-command.mjs +52 -0
- package/dist/internal/session-preferences.mjs +93 -0
- package/dist/internal/stream-resilience.mjs +273 -0
- package/docs/README.en.md +15 -0
- package/docs/README.md +15 -0
- package/docs/architecture.en.md +106 -0
- package/docs/architecture.md +106 -0
- package/docs/compatibility.en.md +53 -0
- package/docs/compatibility.md +53 -0
- package/docs/configuration.en.md +61 -0
- package/docs/configuration.md +61 -0
- package/docs/contribution-sources.en.md +35 -0
- package/docs/contribution-sources.md +35 -0
- package/docs/github-about.md +15 -0
- package/docs/releases/v0.0.1.acceptance.json +160 -0
- package/docs/releases/v0.0.1.md +174 -0
- package/docs/releasing.en.md +290 -0
- package/docs/releasing.md +290 -0
- package/docs/testing.en.md +85 -0
- package/docs/testing.md +85 -0
- package/docs/troubleshooting.en.md +47 -0
- package/docs/troubleshooting.md +47 -0
- package/package.json +144 -0
- package/types/client.d.ts +160 -0
- package/types/index.d.ts +22 -0
- package/types/reliability.d.ts +78 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
const ACCOUNT_QUOTA_CODES = new Set([
|
|
2
|
+
"accountquotaexceeded",
|
|
3
|
+
"billinghardlimitreached",
|
|
4
|
+
"insufficientquota",
|
|
5
|
+
"insufficientcredits",
|
|
6
|
+
"quota",
|
|
7
|
+
"quotaexceeded",
|
|
8
|
+
"usagequotaexceeded",
|
|
9
|
+
])
|
|
10
|
+
|
|
11
|
+
const ACCOUNT_QUOTA_PATTERNS = [
|
|
12
|
+
/\binsufficient[\s_-]+(?:quota|credits?|balance)\b/iu,
|
|
13
|
+
/\byou\s+have\s+exceeded\s+the\s+(?:(?:\d+|five)[\s-]*hour\s+)?usage\s+quota\b/iu,
|
|
14
|
+
/\b(?:chatgpt|account)\s+(?:quota|usage\s+limit)\s+will\s+reset\s+at\b/iu,
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
const PI_AI_AMBIGUOUS_429_PATTERN = /^you\s+have\s+hit\s+your\s+chatgpt\s+usage\s+limit(?:\s+\([^\r\n)]{1,80}\))?\.(?:\s+try\s+again\s+in\s+~\d+\s+min\.)?$/iu
|
|
18
|
+
|
|
19
|
+
const PI_AI_TRANSPORT_PATTERNS = [
|
|
20
|
+
/^websocket\s+closed(?:\s|$)/iu,
|
|
21
|
+
/^websocket\s+stream\s+closed\s+before\s+response\.completed$/iu,
|
|
22
|
+
/^websocket\s+error(?:\s|$)/iu,
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const MAX_FAILURE_TEXT_CHARS = 65_536
|
|
26
|
+
const MAX_EMBEDDED_JSON_CHARS = 32_768
|
|
27
|
+
const MAX_EMBEDDED_OBJECTS = 16
|
|
28
|
+
|
|
29
|
+
const RESET_KEYS = new Set([
|
|
30
|
+
"resetat",
|
|
31
|
+
"resettime",
|
|
32
|
+
"resetsat",
|
|
33
|
+
"quotaresetat",
|
|
34
|
+
"usageresetat",
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
const REQUEST_ID_KEYS = new Set([
|
|
38
|
+
"requestid",
|
|
39
|
+
"requestidentifier",
|
|
40
|
+
"xrequestid",
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
const CLASSIFIER_KEYS = new Set(["code", "type"])
|
|
44
|
+
|
|
45
|
+
function normalizedKey(value) {
|
|
46
|
+
return String(value).replaceAll(/[^a-z0-9]/giu, "").toLowerCase()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function scalarText(value) {
|
|
50
|
+
if (typeof value === "string") return value
|
|
51
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function safeRequestId(value) {
|
|
55
|
+
const candidate = scalarText(value)?.trim()
|
|
56
|
+
if (candidate === undefined || !/^[A-Za-z0-9._:-]{1,128}$/u.test(candidate)) return undefined
|
|
57
|
+
return candidate
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Extract balanced JSON objects without evaluating the surrounding text. */
|
|
61
|
+
export function parseEmbeddedJsonObjects(text) {
|
|
62
|
+
if (typeof text !== "string" || text.length === 0) return []
|
|
63
|
+
|
|
64
|
+
const bounded = text.slice(0, MAX_FAILURE_TEXT_CHARS)
|
|
65
|
+
const values = []
|
|
66
|
+
let start = -1
|
|
67
|
+
let depth = 0
|
|
68
|
+
let quoted = false
|
|
69
|
+
let escaped = false
|
|
70
|
+
|
|
71
|
+
for (let index = 0; index < bounded.length; index += 1) {
|
|
72
|
+
const character = bounded[index]
|
|
73
|
+
if (quoted) {
|
|
74
|
+
if (escaped) escaped = false
|
|
75
|
+
else if (character === "\\") escaped = true
|
|
76
|
+
else if (character === '"') quoted = false
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (character === '"') {
|
|
81
|
+
quoted = true
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
if (character === "{") {
|
|
85
|
+
if (depth === 0) start = index
|
|
86
|
+
depth += 1
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
if (character !== "}" || depth === 0) continue
|
|
90
|
+
|
|
91
|
+
depth -= 1
|
|
92
|
+
if (depth !== 0 || start < 0) continue
|
|
93
|
+
if (index - start + 1 > MAX_EMBEDDED_JSON_CHARS) {
|
|
94
|
+
start = -1
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const value = JSON.parse(bounded.slice(start, index + 1))
|
|
99
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
100
|
+
values.push(value)
|
|
101
|
+
if (values.length >= MAX_EMBEDDED_OBJECTS) return values
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// The provider prefix may contain braces that are not JSON. Ignore them.
|
|
105
|
+
}
|
|
106
|
+
start = -1
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return values
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function knownErrorRecords(values) {
|
|
113
|
+
const records = []
|
|
114
|
+
for (const value of values) {
|
|
115
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) continue
|
|
116
|
+
records.push(value)
|
|
117
|
+
let error
|
|
118
|
+
try {
|
|
119
|
+
error = value.error
|
|
120
|
+
} catch {
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
if (error !== null && typeof error === "object" && !Array.isArray(error)) records.push(error)
|
|
124
|
+
}
|
|
125
|
+
return records
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function withoutEmbeddedObjects(text) {
|
|
129
|
+
let output = ""
|
|
130
|
+
let cursor = 0
|
|
131
|
+
let start = -1
|
|
132
|
+
let depth = 0
|
|
133
|
+
let quoted = false
|
|
134
|
+
let escaped = false
|
|
135
|
+
|
|
136
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
137
|
+
const character = text[index]
|
|
138
|
+
if (quoted) {
|
|
139
|
+
if (escaped) escaped = false
|
|
140
|
+
else if (character === "\\") escaped = true
|
|
141
|
+
else if (character === '"') quoted = false
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
if (character === '"') {
|
|
145
|
+
quoted = true
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
if (character === "{") {
|
|
149
|
+
if (depth === 0) start = index
|
|
150
|
+
depth += 1
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
if (character !== "}" || depth === 0) continue
|
|
154
|
+
depth -= 1
|
|
155
|
+
if (depth !== 0) continue
|
|
156
|
+
output += `${text.slice(cursor, start)} `
|
|
157
|
+
cursor = index + 1
|
|
158
|
+
start = -1
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (depth > 0 && start >= 0) return output + text.slice(cursor, start)
|
|
162
|
+
return output + text.slice(cursor)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseStatus(text, records, explicit) {
|
|
166
|
+
if (Number.isInteger(explicit) && explicit >= 100 && explicit <= 599) return explicit
|
|
167
|
+
for (const record of records) {
|
|
168
|
+
for (const [key, value] of Object.entries(record)) {
|
|
169
|
+
if (!new Set(["status", "statuscode", "httpstatus"]).has(normalizedKey(key))) continue
|
|
170
|
+
const parsed = Number(value)
|
|
171
|
+
if (Number.isInteger(parsed) && parsed >= 100 && parsed <= 599) return parsed
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const hit = text.match(/\b(?:openai\s+api\s+error|api\s+error|http(?:\s+status)?)\s*\(?\s*(\d{3})\s*\)?/iu)
|
|
175
|
+
return hit === null ? undefined : Number(hit[1])
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function explicitOffsetDate(raw) {
|
|
179
|
+
const hit = raw.match(/\b(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})\s*([+-])(\d{2}):?(\d{2})\b/u)
|
|
180
|
+
if (hit === null) return undefined
|
|
181
|
+
const [, year, month, day, hour, minute, second, sign, offsetHour, offsetMinute] = hit
|
|
182
|
+
const values = [year, month, day, hour, minute, second].map(Number)
|
|
183
|
+
const [yearValue, monthValue, dayValue, hourValue, minuteValue, secondValue] = values
|
|
184
|
+
const offsetHourValue = Number(offsetHour)
|
|
185
|
+
const offsetMinuteValue = Number(offsetMinute)
|
|
186
|
+
if (monthValue < 1 || monthValue > 12 || dayValue < 1 || dayValue > 31
|
|
187
|
+
|| hourValue > 23 || minuteValue > 59 || secondValue > 59
|
|
188
|
+
|| offsetHourValue > 23 || offsetMinuteValue > 59) return undefined
|
|
189
|
+
const wallTime = Date.UTC(
|
|
190
|
+
yearValue,
|
|
191
|
+
monthValue - 1,
|
|
192
|
+
dayValue,
|
|
193
|
+
hourValue,
|
|
194
|
+
minuteValue,
|
|
195
|
+
secondValue,
|
|
196
|
+
)
|
|
197
|
+
const wall = new Date(wallTime)
|
|
198
|
+
if (wall.getUTCFullYear() !== yearValue || wall.getUTCMonth() !== monthValue - 1
|
|
199
|
+
|| wall.getUTCDate() !== dayValue || wall.getUTCHours() !== hourValue
|
|
200
|
+
|| wall.getUTCMinutes() !== minuteValue || wall.getUTCSeconds() !== secondValue) return undefined
|
|
201
|
+
const offset = (offsetHourValue * 60 + offsetMinuteValue) * (sign === "+" ? 1 : -1)
|
|
202
|
+
const epochMs = wallTime - offset * 60_000
|
|
203
|
+
if (!Number.isFinite(epochMs)) return undefined
|
|
204
|
+
return {
|
|
205
|
+
raw: `${year}-${month}-${day} ${hour}:${minute}:${second} UTC${sign}${offsetHour}:${offsetMinute}`,
|
|
206
|
+
epochMs,
|
|
207
|
+
iso: new Date(epochMs).toISOString(),
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function explicitZuluDate(raw) {
|
|
212
|
+
const hit = raw.match(/\b(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,3})?Z\b/u)
|
|
213
|
+
if (hit === null) return undefined
|
|
214
|
+
const [, year, month, day, hour, minute, second] = hit
|
|
215
|
+
const [yearValue, monthValue, dayValue, hourValue, minuteValue, secondValue] = [
|
|
216
|
+
year,
|
|
217
|
+
month,
|
|
218
|
+
day,
|
|
219
|
+
hour,
|
|
220
|
+
minute,
|
|
221
|
+
second,
|
|
222
|
+
].map(Number)
|
|
223
|
+
if (monthValue < 1 || monthValue > 12 || dayValue < 1 || dayValue > 31
|
|
224
|
+
|| hourValue > 23 || minuteValue > 59 || secondValue > 59) return undefined
|
|
225
|
+
const epochMs = Date.UTC(
|
|
226
|
+
yearValue,
|
|
227
|
+
monthValue - 1,
|
|
228
|
+
dayValue,
|
|
229
|
+
hourValue,
|
|
230
|
+
minuteValue,
|
|
231
|
+
secondValue,
|
|
232
|
+
)
|
|
233
|
+
const date = new Date(epochMs)
|
|
234
|
+
if (date.getUTCFullYear() !== yearValue || date.getUTCMonth() !== monthValue - 1
|
|
235
|
+
|| date.getUTCDate() !== dayValue || date.getUTCHours() !== hourValue
|
|
236
|
+
|| date.getUTCMinutes() !== minuteValue || date.getUTCSeconds() !== secondValue) return undefined
|
|
237
|
+
const iso = date.toISOString()
|
|
238
|
+
return { raw: iso, epochMs, iso }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function resetCandidate(text, records) {
|
|
242
|
+
for (const record of records) {
|
|
243
|
+
for (const [key, value] of Object.entries(record)) {
|
|
244
|
+
if (!RESET_KEYS.has(normalizedKey(key))) continue
|
|
245
|
+
const candidate = scalarText(value)
|
|
246
|
+
if (candidate !== undefined && candidate.length > 0) return candidate
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const hit = text.match(/\b(?:it\s+will\s+)?reset(?:s)?\s+at\s+(.+?)(?=\.\s+(?:we|please|request)\b|$)/iu)
|
|
250
|
+
return hit?.[1]?.trim()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function parseReset(text, records) {
|
|
254
|
+
const candidate = resetCandidate(text, records)
|
|
255
|
+
if (candidate === undefined || candidate.length > 128 || /[\r\n\0]/u.test(candidate)) return undefined
|
|
256
|
+
const offset = explicitOffsetDate(candidate)
|
|
257
|
+
if (offset !== undefined) return offset
|
|
258
|
+
const zulu = explicitZuluDate(candidate)
|
|
259
|
+
if (zulu !== undefined) return zulu
|
|
260
|
+
const numeric = /^\d{10}(?:\d{3})?$/u.test(candidate)
|
|
261
|
+
? Number(candidate) * (candidate.length === 10 ? 1_000 : 1)
|
|
262
|
+
: undefined
|
|
263
|
+
const epochMs = numeric
|
|
264
|
+
if (!Number.isFinite(epochMs)) return undefined
|
|
265
|
+
const iso = new Date(epochMs).toISOString()
|
|
266
|
+
return { raw: iso, epochMs, iso }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function requestIdCandidate(text, records, explicit) {
|
|
270
|
+
const direct = safeRequestId(explicit)
|
|
271
|
+
if (direct !== undefined) return direct
|
|
272
|
+
for (const record of records) {
|
|
273
|
+
for (const [key, value] of Object.entries(record)) {
|
|
274
|
+
if (!REQUEST_ID_KEYS.has(normalizedKey(key))) continue
|
|
275
|
+
const candidate = safeRequestId(value)
|
|
276
|
+
if (candidate !== undefined) return candidate
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return safeRequestId(
|
|
280
|
+
text.match(/\brequest\s+id\s*[::]\s*([A-Za-z0-9._:-]{1,128})(?=$|[\s,;}"'])/iu)?.[1],
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function structuredClassifiers(records) {
|
|
285
|
+
const values = []
|
|
286
|
+
for (const record of records) {
|
|
287
|
+
for (const [key, value] of Object.entries(record)) {
|
|
288
|
+
if (!CLASSIFIER_KEYS.has(normalizedKey(key))) continue
|
|
289
|
+
const text = scalarText(value)
|
|
290
|
+
if (text !== undefined) values.push(text)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return values
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function structuredMessages(records) {
|
|
297
|
+
const values = []
|
|
298
|
+
for (const record of records) {
|
|
299
|
+
for (const [key, value] of Object.entries(record)) {
|
|
300
|
+
if (normalizedKey(key) !== "message") continue
|
|
301
|
+
const text = scalarText(value)
|
|
302
|
+
if (text !== undefined) values.push(text)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return values
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function hasAccountQuotaClassifier(records) {
|
|
309
|
+
return structuredClassifiers(records)
|
|
310
|
+
.some((code) => ACCOUNT_QUOTA_CODES.has(normalizedKey(code)))
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function hasAccountQuotaMessage(text, records) {
|
|
314
|
+
const joined = [
|
|
315
|
+
text,
|
|
316
|
+
...structuredMessages(records).map((value) => withoutEmbeddedObjects(value)),
|
|
317
|
+
].join("\n")
|
|
318
|
+
return ACCOUNT_QUOTA_PATTERNS.some((pattern) => pattern.test(joined))
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function accountQuotaEvidence(source, message, embeddedObjects) {
|
|
322
|
+
const sourceRecords = knownErrorRecords([source])
|
|
323
|
+
const sourceGroup = {
|
|
324
|
+
classifierRecords: [source],
|
|
325
|
+
messageRecords: [],
|
|
326
|
+
records: [source],
|
|
327
|
+
text: withoutEmbeddedObjects(message),
|
|
328
|
+
}
|
|
329
|
+
const detailGroups = [
|
|
330
|
+
...sourceRecords.slice(1).map((error) => ({
|
|
331
|
+
classifierRecords: [error],
|
|
332
|
+
messageRecords: [error],
|
|
333
|
+
records: [error],
|
|
334
|
+
text: "",
|
|
335
|
+
})),
|
|
336
|
+
...embeddedObjects.map((embedded) => {
|
|
337
|
+
const records = knownErrorRecords([embedded])
|
|
338
|
+
return {
|
|
339
|
+
classifierRecords: records,
|
|
340
|
+
messageRecords: records,
|
|
341
|
+
records,
|
|
342
|
+
text: "",
|
|
343
|
+
}
|
|
344
|
+
}),
|
|
345
|
+
]
|
|
346
|
+
const groups = [sourceGroup, ...detailGroups]
|
|
347
|
+
|
|
348
|
+
const matchingGroup = groups.find((group) => hasAccountQuotaClassifier(group.classifierRecords))
|
|
349
|
+
?? groups.find((group) => hasAccountQuotaMessage(group.text, group.messageRecords))
|
|
350
|
+
if (matchingGroup === undefined || matchingGroup !== sourceGroup) return matchingGroup
|
|
351
|
+
|
|
352
|
+
return detailGroups.find((group) => hasAccountQuotaClassifier(group.classifierRecords))
|
|
353
|
+
?? detailGroups.find((group) => hasAccountQuotaMessage(group.text, group.messageRecords))
|
|
354
|
+
?? sourceGroup
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function isKnownTransportFailure(source, message) {
|
|
358
|
+
const code = normalizedKey(source.code)
|
|
359
|
+
if (code === "streamclosed") return true
|
|
360
|
+
return code === "piaierror"
|
|
361
|
+
&& PI_AI_TRANSPORT_PATTERNS.some((pattern) => pattern.test(message.trim()))
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function isAmbiguousPiAi429(source, message) {
|
|
365
|
+
return normalizedKey(source.code) === "piaierror"
|
|
366
|
+
&& PI_AI_AMBIGUOUS_429_PATTERN.test(message.trim())
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function inspectCodexFailure(failure) {
|
|
370
|
+
const source = failure !== null && typeof failure === "object" ? failure : {}
|
|
371
|
+
const rawMessage = typeof source.message === "string" ? source.message : String(failure ?? "Unknown Codex failure")
|
|
372
|
+
const message = rawMessage.slice(0, MAX_FAILURE_TEXT_CHARS)
|
|
373
|
+
const embeddedObjects = parseEmbeddedJsonObjects(message)
|
|
374
|
+
const quotaEvidence = accountQuotaEvidence(source, message, embeddedObjects)
|
|
375
|
+
const accountQuota = quotaEvidence !== undefined
|
|
376
|
+
const factRecords = accountQuota
|
|
377
|
+
? [source, ...quotaEvidence.records.filter((record) => record !== source)]
|
|
378
|
+
: [source]
|
|
379
|
+
const trustedText = [
|
|
380
|
+
withoutEmbeddedObjects(message),
|
|
381
|
+
...(quotaEvidence === undefined
|
|
382
|
+
? []
|
|
383
|
+
: structuredMessages(quotaEvidence.messageRecords)
|
|
384
|
+
.map((value) => withoutEmbeddedObjects(value))),
|
|
385
|
+
].join("\n")
|
|
386
|
+
const transport = !accountQuota && isKnownTransportFailure(source, message)
|
|
387
|
+
const ambiguous429 = !accountQuota && !transport && isAmbiguousPiAi429(source, message)
|
|
388
|
+
return Object.freeze({
|
|
389
|
+
kind: accountQuota
|
|
390
|
+
? "account-quota"
|
|
391
|
+
: transport
|
|
392
|
+
? "transport"
|
|
393
|
+
: ambiguous429
|
|
394
|
+
? "ambiguous-limit"
|
|
395
|
+
: "other",
|
|
396
|
+
status: parseStatus(trustedText, factRecords, source.status),
|
|
397
|
+
reset: parseReset(trustedText, factRecords),
|
|
398
|
+
requestId: requestIdCandidate(trustedText, factRecords, source.requestId),
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function quotaMessage(facts) {
|
|
403
|
+
const reset = facts.reset?.raw === undefined
|
|
404
|
+
? ""
|
|
405
|
+
: ` 重置时间 / Reset: ${facts.reset.raw}.`
|
|
406
|
+
const request = facts.requestId === undefined
|
|
407
|
+
? ""
|
|
408
|
+
: ` 请求 ID / Request ID: ${facts.requestId}.`
|
|
409
|
+
return `ChatGPT Codex 账户用量配额已耗尽,已停止自动重试。 / ChatGPT Codex account quota exhausted; automatic retry stopped.${reset}${request}`
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function ambiguousLimitMessage() {
|
|
413
|
+
return "pi-ai 返回了无法区分账户配额与临时限流的通用 ChatGPT usage-limit 文案;因原始结构化证据不可用,未标记为账户配额,并已停止自动重试。 / pi-ai returned generalized ChatGPT usage-limit text that cannot distinguish account quota from transient rate limiting; because the original structured evidence is unavailable, it was not marked as account quota, and automatic retry stopped."
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function transportMessage() {
|
|
417
|
+
return "Codex 传输在响应完成前中断。 / Codex transport ended before the response completed."
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Normalize only failures with a public, narrowly verifiable classification. */
|
|
421
|
+
export function normalizeCodexFailure(failure) {
|
|
422
|
+
const facts = inspectCodexFailure(failure)
|
|
423
|
+
if (facts.kind === "transport") {
|
|
424
|
+
return {
|
|
425
|
+
changed: true,
|
|
426
|
+
failure: Object.freeze({
|
|
427
|
+
message: transportMessage(),
|
|
428
|
+
code: "TRANSPORT",
|
|
429
|
+
...(facts.status === undefined ? {} : { status: facts.status }),
|
|
430
|
+
...(facts.requestId === undefined ? {} : { requestId: facts.requestId }),
|
|
431
|
+
}),
|
|
432
|
+
facts,
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (facts.kind === "ambiguous-limit") {
|
|
436
|
+
return {
|
|
437
|
+
changed: true,
|
|
438
|
+
failure: Object.freeze({
|
|
439
|
+
message: ambiguousLimitMessage(),
|
|
440
|
+
code: "QUOTA_OR_RATE_LIMIT",
|
|
441
|
+
...(facts.status === undefined ? {} : { status: facts.status }),
|
|
442
|
+
...(facts.requestId === undefined ? {} : { requestId: facts.requestId }),
|
|
443
|
+
}),
|
|
444
|
+
facts,
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (facts.kind !== "account-quota") return { changed: false, failure, facts }
|
|
448
|
+
|
|
449
|
+
const normalized = {
|
|
450
|
+
message: quotaMessage(facts),
|
|
451
|
+
code: "QUOTA",
|
|
452
|
+
status: facts.status ?? 429,
|
|
453
|
+
...(facts.requestId === undefined ? {} : { requestId: facts.requestId }),
|
|
454
|
+
}
|
|
455
|
+
return { changed: true, failure: Object.freeze(normalized), facts }
|
|
456
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export const DEFAULT_IMAGE_POLICY = Object.freeze({
|
|
2
|
+
maxRequestImageBytes: 20 * 1024 * 1024,
|
|
3
|
+
requestImagePixelBudget: 2048 * 2048,
|
|
4
|
+
requestImageMaxBytes: 1024 * 1024,
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
function positiveSafeInteger(value, name) {
|
|
8
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
9
|
+
throw new TypeError(`${name} must be a positive safe integer`)
|
|
10
|
+
}
|
|
11
|
+
return value
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Resolve every optional image limit at the configuration boundary.
|
|
15
|
+
* Downstream callers receive no optional numeric fields.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveImagePolicy(input = {}) {
|
|
18
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
19
|
+
throw new TypeError("image policy must be an object")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return Object.freeze({
|
|
23
|
+
maxRequestImageBytes: positiveSafeInteger(
|
|
24
|
+
input.maxRequestImageBytes ?? DEFAULT_IMAGE_POLICY.maxRequestImageBytes,
|
|
25
|
+
"maxRequestImageBytes",
|
|
26
|
+
),
|
|
27
|
+
requestImagePixelBudget: positiveSafeInteger(
|
|
28
|
+
input.requestImagePixelBudget ?? DEFAULT_IMAGE_POLICY.requestImagePixelBudget,
|
|
29
|
+
"requestImagePixelBudget",
|
|
30
|
+
),
|
|
31
|
+
requestImageMaxBytes: positiveSafeInteger(
|
|
32
|
+
input.requestImageMaxBytes ?? DEFAULT_IMAGE_POLICY.requestImageMaxBytes,
|
|
33
|
+
"requestImageMaxBytes",
|
|
34
|
+
),
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Convert a resolved provider profile into the attachment service contract. */
|
|
39
|
+
export function toAttachmentRequestPolicy(policy) {
|
|
40
|
+
const resolved = resolveImagePolicy(policy)
|
|
41
|
+
return Object.freeze({
|
|
42
|
+
maxPixels: resolved.requestImagePixelBudget,
|
|
43
|
+
maxBytes: resolved.requestImageMaxBytes,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const DEFAULT_STALE_MS = 5 * 60_000
|
|
2
|
+
const DEFAULT_MAX_RESET_HORIZON_MS = 8 * 24 * 60 * 60_000
|
|
3
|
+
const UNKNOWN_SNAPSHOT = Object.freeze({ status: "unknown" })
|
|
4
|
+
|
|
5
|
+
const OPTION_FIELDS = new Set(["clock", "staleMs", "maxResetHorizonMs"])
|
|
6
|
+
const QUOTA_FIELDS = new Set(["observedAt", "resetAt"])
|
|
7
|
+
|
|
8
|
+
function isPlainObject(value) {
|
|
9
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false
|
|
10
|
+
const prototype = Object.getPrototypeOf(value)
|
|
11
|
+
return prototype === Object.prototype || prototype === null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function plainObject(value, name) {
|
|
15
|
+
if (!isPlainObject(value)) throw new TypeError(`${name} must be a plain object`)
|
|
16
|
+
return value
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function rejectUnknownFields(value, allowed, messagePrefix) {
|
|
20
|
+
for (const field of Reflect.ownKeys(value)) {
|
|
21
|
+
if (typeof field === "string" && allowed.has(field)) continue
|
|
22
|
+
throw new TypeError(`${messagePrefix}: ${String(field)}`)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function timestamp(value, name) {
|
|
27
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
28
|
+
throw new TypeError(`${name} must be a non-negative safe integer`)
|
|
29
|
+
}
|
|
30
|
+
return value
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function positiveDuration(value, name) {
|
|
34
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
35
|
+
throw new TypeError(`${name} must be a positive safe integer`)
|
|
36
|
+
}
|
|
37
|
+
return value
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function staleAt(now, observedAt, staleMs) {
|
|
41
|
+
return now >= observedAt && now - observedAt >= staleMs
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function frozenSnapshot(evidence) {
|
|
45
|
+
if (evidence === undefined) return UNKNOWN_SNAPSHOT
|
|
46
|
+
if (evidence.status === "recent-success") {
|
|
47
|
+
return Object.freeze({
|
|
48
|
+
status: evidence.status,
|
|
49
|
+
observedAt: evidence.observedAt,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
if (evidence.resetAt === undefined) {
|
|
53
|
+
return Object.freeze({
|
|
54
|
+
status: evidence.status,
|
|
55
|
+
observedAt: evidence.observedAt,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
return Object.freeze({
|
|
59
|
+
status: evidence.status,
|
|
60
|
+
observedAt: evidence.observedAt,
|
|
61
|
+
resetAt: evidence.resetAt,
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Observe only quota availability facts. Provider payloads, account data,
|
|
67
|
+
* request identifiers, and error text never cross this module's interface.
|
|
68
|
+
*/
|
|
69
|
+
export function createQuotaObserver(options = {}) {
|
|
70
|
+
const input = plainObject(options, "quota observer options")
|
|
71
|
+
rejectUnknownFields(input, OPTION_FIELDS, "unknown quota observer option")
|
|
72
|
+
|
|
73
|
+
const clock = input.clock === undefined ? Date.now : input.clock
|
|
74
|
+
if (typeof clock !== "function") throw new TypeError("clock must be a function")
|
|
75
|
+
const staleMs = positiveDuration(
|
|
76
|
+
input.staleMs === undefined ? DEFAULT_STALE_MS : input.staleMs,
|
|
77
|
+
"staleMs",
|
|
78
|
+
)
|
|
79
|
+
const maxResetHorizonMs = positiveDuration(
|
|
80
|
+
input.maxResetHorizonMs === undefined ? DEFAULT_MAX_RESET_HORIZON_MS : input.maxResetHorizonMs,
|
|
81
|
+
"maxResetHorizonMs",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
let evidence
|
|
85
|
+
let lastObservedAt = -1
|
|
86
|
+
|
|
87
|
+
function readClock() {
|
|
88
|
+
return timestamp(clock(), "clock()")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function expire(now) {
|
|
92
|
+
if (evidence === undefined) return
|
|
93
|
+
if (evidence.status === "recent-success") {
|
|
94
|
+
if (staleAt(now, evidence.observedAt, staleMs)) evidence = undefined
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
if (evidence.resetAt !== undefined) {
|
|
98
|
+
if (now >= evidence.resetAt) evidence = undefined
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
if (staleAt(now, evidence.observedAt, staleMs)) evidence = undefined
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function snapshot() {
|
|
105
|
+
expire(readClock())
|
|
106
|
+
return frozenSnapshot(evidence)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function observeSuccess(now) {
|
|
110
|
+
const observedAt = timestamp(now, "now")
|
|
111
|
+
if (observedAt < lastObservedAt) return snapshot()
|
|
112
|
+
|
|
113
|
+
lastObservedAt = observedAt
|
|
114
|
+
evidence = { status: "recent-success", observedAt }
|
|
115
|
+
return snapshot()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function observeQuota(observation) {
|
|
119
|
+
const value = plainObject(observation, "quota observation")
|
|
120
|
+
rejectUnknownFields(value, QUOTA_FIELDS, "unknown quota observation field")
|
|
121
|
+
|
|
122
|
+
const observedAt = timestamp(value.observedAt, "observedAt")
|
|
123
|
+
const resetAt = value.resetAt === undefined
|
|
124
|
+
? undefined
|
|
125
|
+
: timestamp(value.resetAt, "resetAt")
|
|
126
|
+
if (observedAt < lastObservedAt) return snapshot()
|
|
127
|
+
|
|
128
|
+
lastObservedAt = observedAt
|
|
129
|
+
if (resetAt !== undefined && resetAt <= observedAt) {
|
|
130
|
+
evidence = undefined
|
|
131
|
+
return snapshot()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const acceptedResetAt = resetAt !== undefined && resetAt - observedAt <= maxResetHorizonMs
|
|
135
|
+
? resetAt
|
|
136
|
+
: undefined
|
|
137
|
+
evidence = { status: "exhausted", observedAt, resetAt: acceptedResetAt }
|
|
138
|
+
return snapshot()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return Object.freeze({ observeSuccess, observeQuota, snapshot })
|
|
142
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export {
|
|
2
|
+
inspectCodexFailure,
|
|
3
|
+
normalizeCodexFailure,
|
|
4
|
+
parseEmbeddedJsonObjects,
|
|
5
|
+
} from "./failure-normalizer.mjs"
|
|
6
|
+
export {
|
|
7
|
+
DEFAULT_IMAGE_POLICY,
|
|
8
|
+
resolveImagePolicy,
|
|
9
|
+
toAttachmentRequestPolicy,
|
|
10
|
+
} from "./image-policy.mjs"
|
|
11
|
+
export { createQuotaObserver } from "./quota-observer.mjs"
|
|
12
|
+
export { stabilizeCodexStream } from "./stream-resilience.mjs"
|