@opengeni/codex 0.2.5 → 0.2.9

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.
@@ -0,0 +1,157 @@
1
+ // Exact Codex rust-v0.144.6 rate-limit-reset-credit protocol normalization.
2
+ // Provenance: stable commit 5d1fbf26c43abc65a203928b2e31561cb039e06d;
3
+ // protocol-bearing files are byte-identical from rust-v0.144.1 through v0.144.6.
4
+ //
5
+ // Upstream sources (stable tag target 5d1fbf26c43abc65a203928b2e31561cb039e06d):
6
+ // - codex-rs/backend-client/src/types.rs
7
+ // - codex-rs/backend-client/src/client/rate_limit_resets.rs
8
+ // - codex-rs/app-server-protocol/src/protocol/v2/account.rs
9
+ //
10
+ // The backend wire is snake_case. Public OpenGeni callers only receive the
11
+ // normalized camelCase types below. Unknown reset types/statuses remain visible
12
+ // but fail closed as `unknown`; they are never made actionable by this parser.
13
+
14
+ import * as z from "zod/v4";
15
+
16
+ export const CODEX_RATE_LIMIT_RESET_OUTCOMES = [
17
+ "reset",
18
+ "nothingToReset",
19
+ "noCredit",
20
+ "alreadyRedeemed",
21
+ ] as const;
22
+
23
+ export type CodexRateLimitResetOutcome = (typeof CODEX_RATE_LIMIT_RESET_OUTCOMES)[number];
24
+ export type CodexRateLimitResetType = "codexRateLimits" | "unknown";
25
+ export type CodexRateLimitResetCreditStatus = "available" | "redeeming" | "redeemed" | "unknown";
26
+
27
+ export type CodexRateLimitResetCredit = {
28
+ id: string;
29
+ resetType: CodexRateLimitResetType;
30
+ status: CodexRateLimitResetCreditStatus;
31
+ /** Unix seconds, matching account/rateLimits/read in Codex v0.144.6. */
32
+ grantedAt: number;
33
+ /** Unix seconds, or null when the provider says the credit does not expire. */
34
+ expiresAt: number | null;
35
+ title: string | null;
36
+ description: string | null;
37
+ };
38
+
39
+ export type CodexRateLimitResetCreditsDetails = {
40
+ availableCount: number;
41
+ credits: CodexRateLimitResetCredit[];
42
+ };
43
+
44
+ export type CodexRateLimitResetCreditsSummary = {
45
+ availableCount: number;
46
+ /** null means the provider supplied an authoritative count but no detail rows. */
47
+ credits: null;
48
+ };
49
+
50
+ export type CodexRateLimitResetConsumeResponse = {
51
+ outcome: CodexRateLimitResetOutcome;
52
+ };
53
+
54
+ const nonNegativeInteger = z.number().int().nonnegative();
55
+ const backendTimestamp = z.string().datetime({ offset: true });
56
+
57
+ const backendCreditSchema = z
58
+ .object({
59
+ id: z.string().min(1),
60
+ reset_type: z.string().min(1),
61
+ status: z.string().min(1),
62
+ granted_at: backendTimestamp,
63
+ expires_at: backendTimestamp.nullish(),
64
+ title: z.string().nullish(),
65
+ description: z.string().nullish(),
66
+ })
67
+ .passthrough();
68
+
69
+ const backendDetailsSchema = z
70
+ .object({
71
+ credits: z.array(backendCreditSchema),
72
+ available_count: nonNegativeInteger,
73
+ })
74
+ .passthrough();
75
+
76
+ const backendUsageSummarySchema = z
77
+ .object({
78
+ rate_limit_reset_credits: z
79
+ .object({ available_count: nonNegativeInteger })
80
+ .passthrough()
81
+ .nullish(),
82
+ })
83
+ .passthrough();
84
+
85
+ const backendConsumeOutcomes = [
86
+ "reset",
87
+ "nothing_to_reset",
88
+ "no_credit",
89
+ "already_redeemed",
90
+ ] as const;
91
+
92
+ const backendConsumeSchema = z
93
+ .object({
94
+ code: z.enum(backendConsumeOutcomes),
95
+ // The app-server intentionally discards this field. OpenGeni also refetches
96
+ // rather than inferring post-redemption state from it.
97
+ windows_reset: nonNegativeInteger.default(0),
98
+ })
99
+ .passthrough();
100
+
101
+ function normalizedResetType(value: string): CodexRateLimitResetType {
102
+ return value === "codex_rate_limits" ? "codexRateLimits" : "unknown";
103
+ }
104
+
105
+ function normalizedCreditStatus(value: string): CodexRateLimitResetCreditStatus {
106
+ if (value === "available" || value === "redeeming" || value === "redeemed") {
107
+ return value;
108
+ }
109
+ return "unknown";
110
+ }
111
+
112
+ /** Parse the exact detailed-credit backend response. Unknown rows stay view-only. */
113
+ export function parseCodexRateLimitResetCreditsDetails(
114
+ payload: unknown,
115
+ ): CodexRateLimitResetCreditsDetails | null {
116
+ const parsed = backendDetailsSchema.safeParse(payload);
117
+ if (!parsed.success) return null;
118
+ return {
119
+ availableCount: parsed.data.available_count,
120
+ credits: parsed.data.credits.map((credit) => ({
121
+ id: credit.id,
122
+ resetType: normalizedResetType(credit.reset_type),
123
+ status: normalizedCreditStatus(credit.status),
124
+ grantedAt: Math.floor(Date.parse(credit.granted_at) / 1000),
125
+ expiresAt:
126
+ credit.expires_at == null ? null : Math.floor(Date.parse(credit.expires_at) / 1000),
127
+ title: credit.title ?? null,
128
+ description: credit.description ?? null,
129
+ })),
130
+ };
131
+ }
132
+
133
+ /** Parse the count-only summary carried by GET /wham/usage. */
134
+ export function parseCodexRateLimitResetCreditsSummary(
135
+ payload: unknown,
136
+ ): CodexRateLimitResetCreditsSummary | null {
137
+ const parsed = backendUsageSummarySchema.safeParse(payload);
138
+ const availableCount = parsed.success
139
+ ? parsed.data.rate_limit_reset_credits?.available_count
140
+ : undefined;
141
+ return availableCount === undefined ? null : { availableCount, credits: null };
142
+ }
143
+
144
+ /** Parse one of the exact four v0.144.6 consume outcomes. Unknowns fail closed. */
145
+ export function parseCodexRateLimitResetConsumeResponse(
146
+ payload: unknown,
147
+ ): CodexRateLimitResetConsumeResponse | null {
148
+ const parsed = backendConsumeSchema.safeParse(payload);
149
+ if (!parsed.success) return null;
150
+ const outcomes: Record<(typeof backendConsumeOutcomes)[number], CodexRateLimitResetOutcome> = {
151
+ reset: "reset",
152
+ nothing_to_reset: "nothingToReset",
153
+ no_credit: "noCredit",
154
+ already_redeemed: "alreadyRedeemed",
155
+ };
156
+ return { outcome: outcomes[parsed.data.code] };
157
+ }
@@ -0,0 +1,159 @@
1
+ import {
2
+ CODEX_RESPONSE_HEADERS_TIMEOUT_MS,
3
+ CODEX_RESPONSE_NO_BYTE_RETRIES,
4
+ CODEX_RESPONSE_RETRY_BACKOFF_MS,
5
+ CODEX_RESPONSE_STREAM_IDLE_TIMEOUT_MS,
6
+ CODEX_RESPONSE_WHOLE_TIMEOUT_MS,
7
+ } from "./constants";
8
+ import type { CodexResponseTimeoutClass, CodexResponseTimeoutPolicy } from "./request-context";
9
+
10
+ export const CODEX_RESPONSE_TIMEOUT_ERROR_TYPE = "opengeni_codex_response_timeout";
11
+
12
+ export const DEFAULT_CODEX_RESPONSE_TIMEOUT_POLICY: CodexResponseTimeoutPolicy = Object.freeze({
13
+ headersTimeoutMs: CODEX_RESPONSE_HEADERS_TIMEOUT_MS,
14
+ streamIdleTimeoutMs: CODEX_RESPONSE_STREAM_IDLE_TIMEOUT_MS,
15
+ wholeRequestTimeoutMs: CODEX_RESPONSE_WHOLE_TIMEOUT_MS,
16
+ noByteRetries: CODEX_RESPONSE_NO_BYTE_RETRIES,
17
+ retryBackoffMs: CODEX_RESPONSE_RETRY_BACKOFF_MS,
18
+ });
19
+
20
+ function positiveFinite(value: number | undefined, fallback: number): number {
21
+ return value !== undefined && Number.isFinite(value) && value > 0 ? value : fallback;
22
+ }
23
+
24
+ export function resolveCodexResponseTimeoutPolicy(
25
+ override: Partial<CodexResponseTimeoutPolicy> | undefined,
26
+ ): CodexResponseTimeoutPolicy {
27
+ return {
28
+ headersTimeoutMs: positiveFinite(
29
+ override?.headersTimeoutMs,
30
+ DEFAULT_CODEX_RESPONSE_TIMEOUT_POLICY.headersTimeoutMs,
31
+ ),
32
+ streamIdleTimeoutMs: positiveFinite(
33
+ override?.streamIdleTimeoutMs,
34
+ DEFAULT_CODEX_RESPONSE_TIMEOUT_POLICY.streamIdleTimeoutMs,
35
+ ),
36
+ wholeRequestTimeoutMs: positiveFinite(
37
+ override?.wholeRequestTimeoutMs,
38
+ DEFAULT_CODEX_RESPONSE_TIMEOUT_POLICY.wholeRequestTimeoutMs,
39
+ ),
40
+ // Automatic replay is fail-closed until the provider operation can be
41
+ // durably read/reconciled. Keep the field for policy/event compatibility,
42
+ // but never let a caller opt back into an unproved retry.
43
+ noByteRetries: 0,
44
+ retryBackoffMs:
45
+ override?.retryBackoffMs !== undefined &&
46
+ Number.isFinite(override.retryBackoffMs) &&
47
+ override.retryBackoffMs >= 0
48
+ ? override.retryBackoffMs
49
+ : DEFAULT_CODEX_RESPONSE_TIMEOUT_POLICY.retryBackoffMs,
50
+ };
51
+ }
52
+
53
+ export class CodexResponseTimeoutError extends Error {
54
+ readonly code = CODEX_RESPONSE_TIMEOUT_ERROR_TYPE;
55
+ readonly type = CODEX_RESPONSE_TIMEOUT_ERROR_TYPE;
56
+
57
+ constructor(
58
+ readonly timeoutClass: CodexResponseTimeoutClass,
59
+ readonly requestId: string,
60
+ readonly responseObserved: boolean,
61
+ message = `Codex response ${timeoutClass.replaceAll("_", " ")} timed out`,
62
+ ) {
63
+ super(message);
64
+ this.name = "CodexResponseTimeoutError";
65
+ }
66
+ }
67
+
68
+ export type CodexResponseTimeoutInfo = {
69
+ timeoutClass: CodexResponseTimeoutClass;
70
+ requestId: string | null;
71
+ responseObserved: boolean;
72
+ message: string;
73
+ };
74
+
75
+ function parseTimeoutClass(value: unknown): CodexResponseTimeoutClass | null {
76
+ return value === "connect" ||
77
+ value === "headers" ||
78
+ value === "idle_stream" ||
79
+ value === "whole_request"
80
+ ? value
81
+ : null;
82
+ }
83
+
84
+ /**
85
+ * Recover structured transport timeouts through SDK wrapping. The optional
86
+ * legacy match is deliberately opt-in: `Request timed out.` alone has no
87
+ * provider provenance and the worker enables it only for a confirmed Codex
88
+ * subscription turn.
89
+ */
90
+ export function classifyCodexResponseTimeoutError(
91
+ error: unknown,
92
+ options: { allowLegacyRequestTimeout?: boolean } = {},
93
+ ): CodexResponseTimeoutInfo | null {
94
+ let current: unknown = error;
95
+ for (let depth = 0; depth < 8 && current && typeof current === "object"; depth += 1) {
96
+ const value = current as Record<string, unknown>;
97
+ const nested =
98
+ value.error && typeof value.error === "object"
99
+ ? (value.error as Record<string, unknown>)
100
+ : undefined;
101
+ const type =
102
+ (typeof value.type === "string" ? value.type : undefined) ??
103
+ (typeof value.code === "string" ? value.code : undefined) ??
104
+ (typeof nested?.type === "string" ? nested.type : undefined) ??
105
+ (typeof nested?.code === "string" ? nested.code : undefined);
106
+ if (type === CODEX_RESPONSE_TIMEOUT_ERROR_TYPE || value.name === "CodexResponseTimeoutError") {
107
+ const klass =
108
+ parseTimeoutClass(value.timeoutClass) ??
109
+ parseTimeoutClass(nested?.timeout_class) ??
110
+ "headers";
111
+ return {
112
+ timeoutClass: klass,
113
+ requestId:
114
+ (typeof value.requestId === "string" ? value.requestId : undefined) ??
115
+ (typeof nested?.request_id === "string" ? nested.request_id : null),
116
+ responseObserved:
117
+ typeof value.responseObserved === "boolean"
118
+ ? value.responseObserved
119
+ : nested?.response_observed === true,
120
+ message:
121
+ (typeof value.message === "string" ? value.message : undefined) ??
122
+ (typeof nested?.message === "string" ? nested.message : "Codex response timed out"),
123
+ };
124
+ }
125
+ current = value.cause;
126
+ }
127
+
128
+ if (options.allowLegacyRequestTimeout && error && typeof error === "object") {
129
+ const value = error as Record<string, unknown>;
130
+ if (
131
+ value.name === "APIConnectionTimeoutError" ||
132
+ (value.message === "Request timed out." && value.name === "Error")
133
+ ) {
134
+ return {
135
+ timeoutClass: "headers",
136
+ requestId: null,
137
+ responseObserved: false,
138
+ message: String(value.message ?? "Request timed out."),
139
+ };
140
+ }
141
+ }
142
+ return null;
143
+ }
144
+
145
+ export function isPreHeadersTimeoutError(error: unknown): CodexResponseTimeoutClass | null {
146
+ const structured = classifyCodexResponseTimeoutError(error);
147
+ if (structured && !structured.responseObserved) {
148
+ return structured.timeoutClass;
149
+ }
150
+ if (!error || typeof error !== "object") return null;
151
+ const value = error as Record<string, unknown>;
152
+ const code = typeof value.code === "string" ? value.code : "";
153
+ const name = typeof value.name === "string" ? value.name : "";
154
+ const message = typeof value.message === "string" ? value.message : String(error);
155
+ if (/^(?:ETIMEDOUT|UND_ERR_CONNECT_TIMEOUT)$/i.test(code) || /ConnectTimeout/i.test(name)) {
156
+ return "connect";
157
+ }
158
+ return /connect(?:ion)?[^.]*timed?\s*out/i.test(`${name} ${message}`) ? "connect" : null;
159
+ }
@@ -11,6 +11,10 @@
11
11
  // limit-reached body. The parser is zod over rate_limit.{primary,secondary}_window.
12
12
 
13
13
  import * as z from "zod/v4";
14
+ import {
15
+ parseCodexRateLimitResetCreditsSummary,
16
+ type CodexRateLimitResetCreditsSummary,
17
+ } from "./reset-credits";
14
18
 
15
19
  /** The 5-hour (primary) window's `limit_window_seconds`. */
16
20
  export const CODEX_FIVE_HOUR_WINDOW_SECONDS = 18000;
@@ -46,12 +50,22 @@ export type CodexUsagePayload = {
46
50
  weekly: CodexUsageWindow | null; // ← rate_limit.secondary_window (604800)
47
51
  limitReached: boolean; // rate_limit.limit_reached || !rate_limit.allowed
48
52
  fetchedAt: string; // ISO; server stamp
53
+ /**
54
+ * Authoritative count-only reset-credit summary from the usage response.
55
+ * Detail rows are fetched separately and are never synthesized from this.
56
+ */
57
+ rateLimitResetCredits: CodexRateLimitResetCreditsSummary | null;
49
58
  /** Present only on a refresh/auth failure path; carries the precise reason. */
50
59
  reason?: "needs_relogin" | undefined;
51
60
  // forward-compat, populated but unused in P2:
52
61
  additionalLimits?: CodexAdditionalLimit[] | undefined;
53
62
  credits?:
54
- | { hasCredits: boolean; unlimited: boolean; overageLimitReached: boolean; balance: string }
63
+ | {
64
+ hasCredits: boolean;
65
+ unlimited: boolean;
66
+ overageLimitReached: boolean;
67
+ balance: string;
68
+ }
55
69
  | undefined;
56
70
  };
57
71
 
@@ -177,7 +191,10 @@ function pickWindows(
177
191
  // Track each unplaced window with the slot it came from, so the positional
178
192
  // fallback can place it (re-normalizing produces a fresh object that would
179
193
  // never match by reference — the bug this replaces).
180
- const unplaced: Array<{ slot: "primary" | "secondary"; window: CodexUsageWindow }> = [];
194
+ const unplaced: Array<{
195
+ slot: "primary" | "secondary";
196
+ window: CodexUsageWindow;
197
+ }> = [];
181
198
  for (const [slot, raw] of [
182
199
  ["primary", primary],
183
200
  ["secondary", secondary],
@@ -219,6 +236,7 @@ export function normalizeCodexUsage(httpStatus: number, rawPayload: unknown): Co
219
236
  weekly: null,
220
237
  limitReached: false,
221
238
  fetchedAt,
239
+ rateLimitResetCredits: parseCodexRateLimitResetCreditsSummary(rawPayload),
222
240
  };
223
241
 
224
242
  // A non-404 HTTP error, or a body we could not parse at all, is an error state.