dsh-codex-community 0.0.1 → 0.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.
@@ -12,6 +12,7 @@ import { createQuotaObserver } from "../internal/quota-observer.mjs"
12
12
  import { createReadImageUrlMiddleware } from "../internal/remote-image-input.mjs"
13
13
  import { createCodexSessionResourceManager } from "../internal/codex-session-resources.mjs"
14
14
  import { registerCodexSessionCommand } from "../internal/session-preference-command.mjs"
15
+ import { registerSessionPreferenceRpc } from "../internal/session-preference-bridge.mjs"
15
16
  import { createSessionPreferences } from "../internal/session-preferences.mjs"
16
17
  import { stabilizeCodexStream } from "../internal/stream-resilience.mjs"
17
18
 
@@ -72,9 +73,11 @@ export function apply(ctx, config = {}) {
72
73
 
73
74
  ctx.inject(["connection"], (connectionCtx) => {
74
75
  installAuthorizationRpc(connectionCtx, {
76
+ accountUsageReader: runtime.accountUsageReader,
75
77
  commitTracker: authorizationCommitTracker,
76
78
  quotaObserver,
77
79
  })
80
+ registerSessionPreferenceRpc(connectionCtx, sessionPreferences)
78
81
  })
79
82
  ctx.inject(["commands"], (commandCtx) => {
80
83
  registerCodexSessionCommand(commandCtx, sessionPreferences, {
@@ -45,6 +45,7 @@ export class CodexAuthorizationBridge {
45
45
  #credentials
46
46
  #commitTracker
47
47
  #quotaObserver
48
+ #accountUsageReader
48
49
  #attempts = new Map()
49
50
  #closed = false
50
51
  #retentionMs
@@ -55,6 +56,7 @@ export class CodexAuthorizationBridge {
55
56
  this.#credentials = credentials
56
57
  this.#commitTracker = options.commitTracker
57
58
  this.#quotaObserver = options.quotaObserver
59
+ this.#accountUsageReader = options.accountUsageReader
58
60
  this.#retentionMs = options.retentionMs ?? ATTEMPT_RETENTION_MS
59
61
  this.#waitMs = options.waitMs ?? STATUS_WAIT_MS
60
62
  }
@@ -203,6 +205,15 @@ export class CodexAuthorizationBridge {
203
205
  return { signedOut: true }
204
206
  }
205
207
 
208
+ async usage(payload = {}, signal) {
209
+ const input = objectInput(payload)
210
+ assertOnlyKeys(input, [])
211
+ if (this.#accountUsageReader?.read === undefined) {
212
+ throw new RpcInputError("Codex account usage is unavailable")
213
+ }
214
+ return this.#accountUsageReader.read({ signal })
215
+ }
216
+
206
217
  dispatch(endpoint, payload, signal) {
207
218
  switch (endpoint) {
208
219
  case "status": return this.status(payload, signal)
@@ -210,6 +221,7 @@ export class CodexAuthorizationBridge {
210
221
  case "respond": return this.respond(payload)
211
222
  case "cancel": return this.cancel(payload)
212
223
  case "logout": return this.logout(payload)
224
+ case "usage": return this.usage(payload, signal)
213
225
  default: throw new RpcInputError("Unknown dsh-codex RPC endpoint")
214
226
  }
215
227
  }
@@ -0,0 +1,509 @@
1
+ import {
2
+ clearTimeout as cancelTimeout,
3
+ setTimeout as scheduleTimeout,
4
+ } from "node:timers"
5
+
6
+ const DEFAULT_TIMEOUT_MS = 10_000
7
+ const DEFAULT_MAX_RESPONSE_BYTES = 128 * 1024
8
+ const MAX_TIMEOUT_MS = 60_000
9
+ const MAX_CONFIGURED_RESPONSE_BYTES = 1024 * 1024
10
+ const MAX_RESET_HORIZON_MS = 366 * 24 * 60 * 60_000
11
+ const MAX_ADDITIONAL_LIMITS = 64
12
+ const MAX_BODY_CHUNKS = 4_096
13
+ const MAX_BASE_URL_LENGTH = 2_048
14
+ const MAX_ACCESS_LENGTH = 64 * 1024
15
+ const MAX_ACCOUNT_ID_LENGTH = 1_024
16
+ const MAX_LABEL_LENGTH = 128
17
+
18
+ const OPTION_FIELDS = new Set([
19
+ "baseUrl",
20
+ "clock",
21
+ "fetch",
22
+ "maxResponseBytes",
23
+ "resolveAuth",
24
+ "timeoutMs",
25
+ ])
26
+ const READ_OPTION_FIELDS = new Set(["signal"])
27
+
28
+ export class CodexAccountUsageError extends Error {
29
+ constructor(message, code, details = {}) {
30
+ super(message)
31
+ this.name = "CodexAccountUsageError"
32
+ this.code = code
33
+ if (details.status !== undefined) this.status = details.status
34
+ }
35
+ }
36
+
37
+ function isPlainObject(value) {
38
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false
39
+ const prototype = Object.getPrototypeOf(value)
40
+ return prototype === Object.prototype || prototype === null
41
+ }
42
+
43
+ function plainObject(value, name) {
44
+ if (!isPlainObject(value)) throw new TypeError(`${name} must be a plain object`)
45
+ return value
46
+ }
47
+
48
+ function rejectUnknownFields(value, allowed, prefix) {
49
+ for (const key of Reflect.ownKeys(value)) {
50
+ if (typeof key === "string" && allowed.has(key)) continue
51
+ throw new TypeError(`${prefix}: ${String(key)}`)
52
+ }
53
+ }
54
+
55
+ function boundedPositiveInteger(value, name, maximum) {
56
+ if (!Number.isSafeInteger(value) || value <= 0 || value > maximum) {
57
+ throw new TypeError(`${name} must be a positive safe integer no greater than ${String(maximum)}`)
58
+ }
59
+ return value
60
+ }
61
+
62
+ function readClock(clock) {
63
+ let now
64
+ try {
65
+ now = clock()
66
+ } catch {
67
+ throw new CodexAccountUsageError("account usage clock returned an invalid timestamp", "INVALID_CLOCK")
68
+ }
69
+ if (!Number.isSafeInteger(now) || now < 0) {
70
+ throw new CodexAccountUsageError("account usage clock returned an invalid timestamp", "INVALID_CLOCK")
71
+ }
72
+ return now
73
+ }
74
+
75
+ function usageUrl(rawBaseUrl) {
76
+ if (typeof rawBaseUrl !== "string" || rawBaseUrl.length === 0 || rawBaseUrl.length > MAX_BASE_URL_LENGTH) {
77
+ throw new TypeError("baseUrl must be a non-empty bounded string")
78
+ }
79
+
80
+ let baseUrl
81
+ try {
82
+ baseUrl = new URL(rawBaseUrl)
83
+ } catch {
84
+ throw new TypeError("baseUrl must be a valid HTTPS URL")
85
+ }
86
+ if (
87
+ baseUrl.protocol !== "https:"
88
+ || baseUrl.username !== ""
89
+ || baseUrl.password !== ""
90
+ || baseUrl.search !== ""
91
+ || baseUrl.hash !== ""
92
+ ) {
93
+ throw new TypeError("baseUrl must be a credential-free HTTPS URL without query or fragment")
94
+ }
95
+ if (!baseUrl.pathname.endsWith("/")) baseUrl.pathname += "/"
96
+ return new URL("wham/usage", baseUrl).href
97
+ }
98
+
99
+ function assertAbortSignal(value) {
100
+ if (value === undefined) return
101
+ if (!(value instanceof AbortSignal)) throw new TypeError("signal must be an AbortSignal")
102
+ }
103
+
104
+ function deadline(parentSignal, timeoutMs) {
105
+ const controller = new AbortController()
106
+ const abortFromParent = () => {
107
+ controller.abort(new CodexAccountUsageError("account usage request was aborted", "ABORTED"))
108
+ }
109
+
110
+ if (parentSignal?.aborted === true) abortFromParent()
111
+ else parentSignal?.addEventListener("abort", abortFromParent, { once: true })
112
+
113
+ let timer
114
+ if (!controller.signal.aborted) {
115
+ timer = scheduleTimeout(() => {
116
+ controller.abort(new CodexAccountUsageError("account usage request timed out", "TIMEOUT"))
117
+ }, timeoutMs)
118
+ }
119
+
120
+ return {
121
+ signal: controller.signal,
122
+ dispose() {
123
+ if (timer !== undefined) cancelTimeout(timer)
124
+ parentSignal?.removeEventListener("abort", abortFromParent)
125
+ },
126
+ }
127
+ }
128
+
129
+ function abortReason(signal) {
130
+ if (signal.reason instanceof CodexAccountUsageError) return signal.reason
131
+ return new CodexAccountUsageError("account usage request was aborted", "ABORTED")
132
+ }
133
+
134
+ function awaitWithSignal(value, signal) {
135
+ if (signal.aborted) return Promise.reject(abortReason(signal))
136
+ return new Promise((resolve, reject) => {
137
+ const onAbort = () => reject(abortReason(signal))
138
+ signal.addEventListener("abort", onAbort, { once: true })
139
+ Promise.resolve(value).then(
140
+ (result) => {
141
+ signal.removeEventListener("abort", onAbort)
142
+ resolve(result)
143
+ },
144
+ (error) => {
145
+ signal.removeEventListener("abort", onAbort)
146
+ reject(error)
147
+ },
148
+ )
149
+ })
150
+ }
151
+
152
+ function safeHeaderValue(value, name, maximum, allowWhitespace) {
153
+ if (
154
+ typeof value !== "string"
155
+ || value.length === 0
156
+ || value.length > maximum
157
+ || value.trim() !== value
158
+ || /[\u0000-\u001f\u007f]/u.test(value)
159
+ || (!allowWhitespace && /\s/u.test(value))
160
+ ) {
161
+ throw new CodexAccountUsageError(`account usage ${name} is unavailable`, "AUTH_UNAVAILABLE")
162
+ }
163
+ return value
164
+ }
165
+
166
+ async function resolveRequestAuth(resolveAuth, signal) {
167
+ let resolved
168
+ try {
169
+ resolved = await awaitWithSignal(
170
+ Promise.resolve().then(() => resolveAuth({ signal })),
171
+ signal,
172
+ )
173
+ } catch (error) {
174
+ if (signal.aborted) throw abortReason(signal)
175
+ if (error instanceof CodexAccountUsageError) throw error
176
+ throw new CodexAccountUsageError("account usage authentication is unavailable", "AUTH_UNAVAILABLE")
177
+ }
178
+
179
+ if (!isPlainObject(resolved)) {
180
+ throw new CodexAccountUsageError("account usage authentication is unavailable", "AUTH_UNAVAILABLE")
181
+ }
182
+ try {
183
+ return {
184
+ access: safeHeaderValue(resolved.access, "access token", MAX_ACCESS_LENGTH, false),
185
+ accountId: safeHeaderValue(resolved.accountId, "account identifier", MAX_ACCOUNT_ID_LENGTH, true),
186
+ }
187
+ } catch (error) {
188
+ if (error instanceof CodexAccountUsageError) throw error
189
+ throw new CodexAccountUsageError("account usage authentication is unavailable", "AUTH_UNAVAILABLE")
190
+ }
191
+ }
192
+
193
+ function discardResponse(response) {
194
+ try {
195
+ const cancellation = response?.body?.cancel?.()
196
+ cancellation?.catch?.(() => {})
197
+ } catch {
198
+ // Discard failures never replace the safe protocol error.
199
+ }
200
+ }
201
+
202
+ function responseHeader(response, name) {
203
+ try {
204
+ return response?.headers?.get?.(name) ?? null
205
+ } catch {
206
+ throw new CodexAccountUsageError("account usage response headers are invalid", "INVALID_RESPONSE")
207
+ }
208
+ }
209
+
210
+ function assertJsonContentType(response) {
211
+ const raw = responseHeader(response, "content-type")
212
+ const mediaType = raw?.split(";", 1)[0].trim().toLowerCase()
213
+ if (
214
+ mediaType !== "application/json"
215
+ && !/^application\/[a-z0-9!#$&^_.+-]+\+json$/u.test(mediaType ?? "")
216
+ ) {
217
+ throw new CodexAccountUsageError("account usage response is not JSON", "INVALID_CONTENT_TYPE")
218
+ }
219
+ }
220
+
221
+ function assertContentLength(response, maxResponseBytes) {
222
+ const raw = responseHeader(response, "content-length")
223
+ if (raw === null) return
224
+ if (!/^(?:0|[1-9]\d*)$/u.test(raw)) {
225
+ throw new CodexAccountUsageError("account usage response length is invalid", "INVALID_RESPONSE")
226
+ }
227
+ const length = Number(raw)
228
+ if (!Number.isSafeInteger(length)) {
229
+ throw new CodexAccountUsageError("account usage response length is invalid", "INVALID_RESPONSE")
230
+ }
231
+ if (length > maxResponseBytes) {
232
+ throw new CodexAccountUsageError("account usage response exceeds the byte limit", "BODY_TOO_LARGE")
233
+ }
234
+ }
235
+
236
+ async function readBoundedBody(response, maxResponseBytes, signal) {
237
+ let reader
238
+ try {
239
+ reader = response?.body?.getReader?.()
240
+ } catch {
241
+ throw new CodexAccountUsageError("account usage response body is unavailable", "INVALID_RESPONSE")
242
+ }
243
+ if (reader === undefined) {
244
+ throw new CodexAccountUsageError("account usage response body is unavailable", "INVALID_RESPONSE")
245
+ }
246
+
247
+ const chunks = []
248
+ let total = 0
249
+ let chunkCount = 0
250
+ try {
251
+ while (true) {
252
+ const part = await awaitWithSignal(reader.read(), signal)
253
+ if (part.done) break
254
+ if (!(part.value instanceof Uint8Array)) {
255
+ throw new CodexAccountUsageError("account usage response body is invalid", "INVALID_RESPONSE")
256
+ }
257
+ total += part.value.byteLength
258
+ if (total > maxResponseBytes) {
259
+ throw new CodexAccountUsageError("account usage response exceeds the byte limit", "BODY_TOO_LARGE")
260
+ }
261
+ chunkCount += 1
262
+ if (chunkCount > MAX_BODY_CHUNKS) {
263
+ throw new CodexAccountUsageError("account usage response body is invalid", "INVALID_RESPONSE")
264
+ }
265
+ chunks.push(part.value)
266
+ }
267
+ } catch (error) {
268
+ try {
269
+ const cancellation = reader.cancel()
270
+ cancellation?.catch?.(() => {})
271
+ } catch {
272
+ // Cancellation failures never replace the safe body error.
273
+ }
274
+ if (signal.aborted) throw abortReason(signal)
275
+ if (error instanceof CodexAccountUsageError) throw error
276
+ throw new CodexAccountUsageError("account usage response body is invalid", "INVALID_RESPONSE")
277
+ }
278
+
279
+ const body = new Uint8Array(total)
280
+ let offset = 0
281
+ for (const chunk of chunks) {
282
+ body.set(chunk, offset)
283
+ offset += chunk.byteLength
284
+ }
285
+ return body
286
+ }
287
+
288
+ async function readJson(response, maxResponseBytes, signal) {
289
+ assertJsonContentType(response)
290
+ assertContentLength(response, maxResponseBytes)
291
+ const body = await readBoundedBody(response, maxResponseBytes, signal)
292
+ if (body.byteLength === 0) {
293
+ throw new CodexAccountUsageError("account usage response body is invalid", "INVALID_RESPONSE")
294
+ }
295
+
296
+ let text
297
+ try {
298
+ text = new TextDecoder("utf-8", { fatal: true }).decode(body)
299
+ } catch {
300
+ throw new CodexAccountUsageError("account usage response body is invalid", "INVALID_RESPONSE")
301
+ }
302
+
303
+ try {
304
+ return JSON.parse(text)
305
+ } catch {
306
+ throw new CodexAccountUsageError("account usage response body is invalid", "INVALID_RESPONSE")
307
+ }
308
+ }
309
+
310
+ function safeShortString(value, name, { nullable = false } = {}) {
311
+ if (nullable && (value === undefined || value === null)) return null
312
+ if (
313
+ typeof value !== "string"
314
+ || value.length === 0
315
+ || value.length > MAX_LABEL_LENGTH
316
+ || value.trim() !== value
317
+ || /[\u0000-\u001f\u007f]/u.test(value)
318
+ ) {
319
+ throw new CodexAccountUsageError(`account usage ${name} is invalid`, "INVALID_RESPONSE")
320
+ }
321
+ return value
322
+ }
323
+
324
+ function optionalConsistentField(envelope, bucket, field) {
325
+ const outer = envelope[field]
326
+ const inner = bucket[field]
327
+ if (outer !== undefined && inner !== undefined && outer !== inner) {
328
+ throw new CodexAccountUsageError("account usage limit identity is inconsistent", "INVALID_RESPONSE")
329
+ }
330
+ return outer ?? inner
331
+ }
332
+
333
+ function normalizeWindow(raw, observedAt) {
334
+ if (raw === undefined || raw === null) return undefined
335
+ if (!isPlainObject(raw)) {
336
+ throw new CodexAccountUsageError("account usage quota window is invalid", "INVALID_RESPONSE")
337
+ }
338
+
339
+ const usedPercent = raw.used_percent
340
+ if (typeof usedPercent !== "number" || !Number.isFinite(usedPercent) || usedPercent < 0 || usedPercent > 100) {
341
+ throw new CodexAccountUsageError("account usage percentage is invalid", "INVALID_RESPONSE")
342
+ }
343
+
344
+ const seconds = raw.limit_window_seconds
345
+ if (!Number.isSafeInteger(seconds) || seconds <= 0 || seconds % 60 !== 0) {
346
+ throw new CodexAccountUsageError("account usage window duration is invalid", "INVALID_RESPONSE")
347
+ }
348
+
349
+ const resetSeconds = raw.reset_at
350
+ if (
351
+ !Number.isSafeInteger(resetSeconds)
352
+ || resetSeconds < 0
353
+ || resetSeconds > Math.floor(Number.MAX_SAFE_INTEGER / 1_000)
354
+ ) {
355
+ throw new CodexAccountUsageError("account usage reset timestamp is invalid", "INVALID_RESPONSE")
356
+ }
357
+ const resetsAt = resetSeconds * 1_000
358
+ if (resetsAt <= observedAt || resetsAt - observedAt > MAX_RESET_HORIZON_MS) {
359
+ throw new CodexAccountUsageError("account usage reset timestamp is implausible", "INVALID_RESPONSE")
360
+ }
361
+
362
+ return Object.freeze({
363
+ usedPercent,
364
+ windowDurationMins: seconds / 60,
365
+ resetsAt,
366
+ })
367
+ }
368
+
369
+ function normalizeLimit(envelope, bucket, observedAt, fallbackId) {
370
+ if (!isPlainObject(envelope) || !isPlainObject(bucket)) {
371
+ throw new CodexAccountUsageError("account usage rate limit is invalid", "INVALID_RESPONSE")
372
+ }
373
+ const explicitId = optionalConsistentField(envelope, bucket, "limit_id")
374
+ const explicitName = optionalConsistentField(envelope, bucket, "limit_name")
375
+ const limitName = safeShortString(explicitName, "limit name", { nullable: true })
376
+ const limitId = safeShortString(explicitId ?? fallbackId ?? limitName, "limit identifier")
377
+ const primary = normalizeWindow(bucket.primary_window, observedAt)
378
+ const secondary = normalizeWindow(bucket.secondary_window, observedAt)
379
+
380
+ const result = {
381
+ limitId,
382
+ limitName,
383
+ }
384
+ if (primary !== undefined) result.primary = primary
385
+ if (secondary !== undefined) result.secondary = secondary
386
+ return Object.freeze(result)
387
+ }
388
+
389
+ function normalizePayload(payload, observedAt) {
390
+ if (!isPlainObject(payload) || !isPlainObject(payload.rate_limit)) {
391
+ throw new CodexAccountUsageError("account usage response shape is invalid", "INVALID_RESPONSE")
392
+ }
393
+
394
+ const main = normalizeLimit(payload, payload.rate_limit, observedAt, "codex")
395
+ const limits = [main]
396
+ const seen = new Set([main.limitId])
397
+ const additional = payload.additional_rate_limits ?? []
398
+ if (!Array.isArray(additional) || additional.length > MAX_ADDITIONAL_LIMITS) {
399
+ throw new CodexAccountUsageError("account usage additional limits are invalid", "INVALID_RESPONSE")
400
+ }
401
+ for (const entry of additional) {
402
+ if (!isPlainObject(entry) || !isPlainObject(entry.rate_limit)) {
403
+ throw new CodexAccountUsageError("account usage additional limit is invalid", "INVALID_RESPONSE")
404
+ }
405
+ const limit = normalizeLimit(entry, entry.rate_limit, observedAt)
406
+ if (seen.has(limit.limitId)) {
407
+ throw new CodexAccountUsageError("account usage limit identifier is duplicated", "INVALID_RESPONSE")
408
+ }
409
+ seen.add(limit.limitId)
410
+ limits.push(limit)
411
+ }
412
+
413
+ const result = {
414
+ observedAt,
415
+ rateLimits: Object.freeze(limits),
416
+ }
417
+ if (payload.plan_type !== undefined && payload.plan_type !== null) {
418
+ result.planType = safeShortString(payload.plan_type, "plan type")
419
+ }
420
+ return Object.freeze(result)
421
+ }
422
+
423
+ function responseMetadata(response) {
424
+ try {
425
+ return {
426
+ redirected: response?.redirected === true,
427
+ status: response?.status,
428
+ }
429
+ } catch {
430
+ throw new CodexAccountUsageError("account usage response metadata is invalid", "INVALID_RESPONSE")
431
+ }
432
+ }
433
+
434
+ /**
435
+ * Build a host-side reader for the authenticated Codex account usage endpoint.
436
+ * `resolveAuth` owns pi-ai-compatible refresh/serialization and must return the
437
+ * resulting OAuth credential as `{ access, accountId }`.
438
+ */
439
+ export function createCodexAccountUsageReader(options) {
440
+ const input = plainObject(options, "account usage reader options")
441
+ rejectUnknownFields(input, OPTION_FIELDS, "unknown account usage reader option")
442
+
443
+ const endpoint = usageUrl(input.baseUrl)
444
+ const resolveAuth = input.resolveAuth
445
+ const fetchImpl = input.fetch ?? globalThis.fetch
446
+ const clock = input.clock ?? Date.now
447
+ if (typeof resolveAuth !== "function") throw new TypeError("resolveAuth must be a function")
448
+ if (typeof fetchImpl !== "function") throw new TypeError("fetch must be a function")
449
+ if (typeof clock !== "function") throw new TypeError("clock must be a function")
450
+ const timeoutMs = boundedPositiveInteger(input.timeoutMs ?? DEFAULT_TIMEOUT_MS, "timeoutMs", MAX_TIMEOUT_MS)
451
+ const maxResponseBytes = boundedPositiveInteger(
452
+ input.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES,
453
+ "maxResponseBytes",
454
+ MAX_CONFIGURED_RESPONSE_BYTES,
455
+ )
456
+
457
+ async function read(readOptions = {}) {
458
+ const request = plainObject(readOptions, "account usage read options")
459
+ rejectUnknownFields(request, READ_OPTION_FIELDS, "unknown account usage read option")
460
+ assertAbortSignal(request.signal)
461
+ const active = deadline(request.signal, timeoutMs)
462
+
463
+ try {
464
+ if (active.signal.aborted) throw abortReason(active.signal)
465
+ const auth = await resolveRequestAuth(resolveAuth, active.signal)
466
+
467
+ let response
468
+ try {
469
+ response = await awaitWithSignal(fetchImpl(endpoint, {
470
+ method: "GET",
471
+ redirect: "error",
472
+ headers: {
473
+ Accept: "application/json",
474
+ Authorization: `Bearer ${auth.access}`,
475
+ "ChatGPT-Account-Id": auth.accountId,
476
+ },
477
+ signal: active.signal,
478
+ }), active.signal)
479
+ } catch (error) {
480
+ if (active.signal.aborted) throw abortReason(active.signal)
481
+ if (error instanceof CodexAccountUsageError) throw error
482
+ throw new CodexAccountUsageError("account usage network request failed", "NETWORK")
483
+ }
484
+
485
+ const metadata = responseMetadata(response)
486
+ const status = metadata.status
487
+ if (!Number.isInteger(status) || status < 100 || status > 599) {
488
+ discardResponse(response)
489
+ throw new CodexAccountUsageError("account usage response status is invalid", "INVALID_RESPONSE")
490
+ }
491
+ if (metadata.redirected || (status >= 300 && status < 400)) {
492
+ discardResponse(response)
493
+ throw new CodexAccountUsageError("account usage response redirected", "REDIRECT")
494
+ }
495
+ if (status < 200 || status >= 300) {
496
+ discardResponse(response)
497
+ throw new CodexAccountUsageError("account usage request returned an HTTP error", "HTTP_STATUS", { status })
498
+ }
499
+
500
+ const payload = await readJson(response, maxResponseBytes, active.signal)
501
+ const observedAt = readClock(clock)
502
+ return normalizePayload(payload, observedAt)
503
+ } finally {
504
+ active.dispose()
505
+ }
506
+ }
507
+
508
+ return Object.freeze({ read })
509
+ }
@@ -0,0 +1,90 @@
1
+ const EFFORT_NAMES = Object.freeze({
2
+ low: "Low",
3
+ medium: "Medium",
4
+ high: "High",
5
+ xhigh: "Xhigh",
6
+ max: "Max",
7
+ })
8
+
9
+ const MODEL_CAPABILITIES = new Map([
10
+ ["gpt-5.3-codex-spark", modelCapability({
11
+ defaultReasoningEffort: "high",
12
+ reasoningEfforts: ["low", "medium", "high", "xhigh"],
13
+ })],
14
+ ["gpt-5.4", modelCapability({
15
+ defaultReasoningEffort: "medium",
16
+ reasoningEfforts: ["low", "medium", "high", "xhigh"],
17
+ fast: true,
18
+ })],
19
+ ["gpt-5.4-mini", modelCapability({
20
+ defaultReasoningEffort: "medium",
21
+ reasoningEfforts: ["low", "medium", "high", "xhigh"],
22
+ })],
23
+ ["gpt-5.5", modelCapability({
24
+ defaultReasoningEffort: "medium",
25
+ reasoningEfforts: ["low", "medium", "high", "xhigh"],
26
+ fast: true,
27
+ })],
28
+ ["gpt-5.6-luna", modelCapability({
29
+ defaultReasoningEffort: "medium",
30
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
31
+ fast: true,
32
+ })],
33
+ ["gpt-5.6-sol", modelCapability({
34
+ defaultReasoningEffort: "low",
35
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
36
+ fast: true,
37
+ })],
38
+ ["gpt-5.6-terra", modelCapability({
39
+ defaultReasoningEffort: "medium",
40
+ reasoningEfforts: ["low", "medium", "high", "xhigh", "max"],
41
+ fast: true,
42
+ })],
43
+ ])
44
+
45
+ function modelCapability({ defaultReasoningEffort, reasoningEfforts, fast = false }) {
46
+ if (!reasoningEfforts.includes(defaultReasoningEffort)) {
47
+ throw new TypeError("defaultReasoningEffort must be included in reasoningEfforts")
48
+ }
49
+ return Object.freeze({
50
+ defaultReasoningEffort,
51
+ reasoningEfforts: Object.freeze(reasoningEfforts.map((id) => Object.freeze({
52
+ id,
53
+ name: EFFORT_NAMES[id],
54
+ }))),
55
+ fast,
56
+ })
57
+ }
58
+
59
+ /**
60
+ * Return the Codex model controls verified from the first-party model catalog.
61
+ * Unknown future models deliberately expose no inferred controls.
62
+ */
63
+ export function codexModelCapability(modelId) {
64
+ return MODEL_CAPABILITIES.get(modelId)
65
+ }
66
+
67
+ export function supportsCodexFast(modelId) {
68
+ return MODEL_CAPABILITIES.get(modelId)?.fast === true
69
+ }
70
+
71
+ export function supportsCodexReasoningEffort(modelId, reasoningEffort) {
72
+ return MODEL_CAPABILITIES.get(modelId)?.reasoningEfforts.some(
73
+ ({ id }) => id === reasoningEffort,
74
+ ) === true
75
+ }
76
+
77
+ export function piThinkingLevelMap(modelId) {
78
+ const capability = MODEL_CAPABILITIES.get(modelId)
79
+ if (capability === undefined) return undefined
80
+ const supported = new Set(capability.reasoningEfforts.map(({ id }) => id))
81
+ return Object.freeze({
82
+ off: null,
83
+ minimal: null,
84
+ low: supported.has("low") ? "low" : null,
85
+ medium: supported.has("medium") ? "medium" : null,
86
+ high: supported.has("high") ? "high" : null,
87
+ xhigh: supported.has("xhigh") ? "xhigh" : null,
88
+ max: supported.has("max") ? "max" : null,
89
+ })
90
+ }