@opengeni/codex 0.2.3 → 0.2.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/codex",
3
- "version": "0.2.3",
3
+ "version": "0.2.7",
4
4
  "description": "ChatGPT/Codex subscription auth + transport: device-code login, token refresh, and the Responses-backend fetch. Pure HTTP + transforms; no database dependency.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/src/api-client.ts CHANGED
@@ -3,6 +3,13 @@
3
3
 
4
4
  import { CODEX_ORIGINATOR, CODEX_RESPONSES_BASE, CODEX_WHAM_BASE } from "./constants";
5
5
  import type { CodexFetch } from "./device-code";
6
+ import {
7
+ parseCodexRateLimitResetConsumeResponse,
8
+ parseCodexRateLimitResetCreditsDetails,
9
+ type CodexRateLimitResetConsumeResponse,
10
+ type CodexRateLimitResetCreditsDetails,
11
+ } from "./reset-credits";
12
+ import { runBoundedCodexOperation } from "./bounded-operation";
6
13
 
7
14
  export type CodexAuthHeaders = {
8
15
  accessToken: string;
@@ -11,6 +18,16 @@ export type CodexAuthHeaders = {
11
18
  clientVersion: string;
12
19
  };
13
20
 
21
+ const CODEX_READ_TIMEOUT_MS = 5_000;
22
+ const RESET_CREDIT_DETAILS_TIMEOUT_MS = 5_000;
23
+ const RESET_CREDIT_CONSUME_TIMEOUT_MS = 10_000;
24
+
25
+ export type ResetCreditFetchFailureReason =
26
+ | "http_error"
27
+ | "invalid_response"
28
+ | "network_error"
29
+ | "timeout";
30
+
14
31
  function subscriptionHeaders(a: CodexAuthHeaders): Record<string, string> {
15
32
  return {
16
33
  Authorization: `Bearer ${a.accessToken}`,
@@ -26,34 +43,126 @@ function subscriptionHeaders(a: CodexAuthHeaders): Record<string, string> {
26
43
  export async function fetchCodexModels(
27
44
  a: CodexAuthHeaders,
28
45
  fetchImpl: CodexFetch = fetch,
46
+ timeoutMs = CODEX_READ_TIMEOUT_MS,
29
47
  ): Promise<{ ok: boolean; status: number; slugs: string[] }> {
30
- const res = await fetchImpl(
31
- `${CODEX_RESPONSES_BASE}/models?client_version=${encodeURIComponent(a.clientVersion)}`,
32
- {
33
- method: "GET",
34
- headers: subscriptionHeaders(a),
35
- },
36
- );
37
- if (!res.ok) {
38
- return { ok: false, status: res.status, slugs: [] };
39
- }
40
- const body = (await res.json()) as { models?: Array<{ slug?: string }> };
41
- const slugs = (body.models ?? [])
42
- .map((m) => m.slug)
43
- .filter((s): s is string => typeof s === "string");
44
- return { ok: true, status: res.status, slugs };
48
+ const fetched = await runBoundedCodexOperation(async (signal) => {
49
+ const res = await fetchImpl(
50
+ `${CODEX_RESPONSES_BASE}/models?client_version=${encodeURIComponent(a.clientVersion)}`,
51
+ { method: "GET", headers: subscriptionHeaders(a), signal },
52
+ );
53
+ if (!res.ok) {
54
+ await res.arrayBuffer().catch(() => undefined);
55
+ return { ok: false, status: res.status, slugs: [] as string[] };
56
+ }
57
+ const body = (await res.json()) as { models?: Array<{ slug?: string }> };
58
+ const slugs = (body.models ?? [])
59
+ .map((model) => model.slug)
60
+ .filter((slug): slug is string => typeof slug === "string");
61
+ return { ok: true, status: res.status, slugs };
62
+ }, timeoutMs);
63
+ return fetched.ok ? fetched.value : { ok: false, status: 0, slugs: [] };
45
64
  }
46
65
 
47
66
  /** GET /wham/usage — authoritative limits. NB the WHAM base is /backend-api, NOT /codex (spec §1.8a). */
48
67
  export async function fetchCodexUsage(
49
68
  a: CodexAuthHeaders,
50
69
  fetchImpl: CodexFetch = fetch,
70
+ timeoutMs = CODEX_READ_TIMEOUT_MS,
51
71
  ): Promise<{ status: number; payload: unknown }> {
52
- const res = await fetchImpl(`${CODEX_WHAM_BASE}/wham/usage`, {
53
- method: "GET",
54
- headers: subscriptionHeaders(a),
55
- });
56
- // A 404 may carry a usage-limit body; the route layer normalizes it to a limits state (spec §1.8c).
57
- const payload = res.ok || res.status === 404 ? await res.json().catch(() => null) : null;
58
- return { status: res.status, payload };
72
+ const fetched = await runBoundedCodexOperation(async (signal) => {
73
+ const res = await fetchImpl(`${CODEX_WHAM_BASE}/wham/usage`, {
74
+ method: "GET",
75
+ headers: subscriptionHeaders(a),
76
+ signal,
77
+ });
78
+ // A 404 may carry a usage-limit body; the route layer normalizes it to a limits state (spec §1.8c).
79
+ const payload = res.ok || res.status === 404 ? await res.json().catch(() => null) : null;
80
+ if (!res.ok && res.status !== 404) await res.arrayBuffer().catch(() => undefined);
81
+ return { status: res.status, payload };
82
+ }, timeoutMs);
83
+ if (!fetched.ok) throw new Error(`Codex usage request ${fetched.reason}`);
84
+ return fetched.value;
85
+ }
86
+
87
+ /**
88
+ * GET /wham/rate-limit-reset-credits — detailed earned reset credits.
89
+ *
90
+ * A non-2xx or malformed body returns an explicit non-ok result. The caller may
91
+ * fall back to the count-only summary embedded in /wham/usage, but must never
92
+ * invent actionable rows from that count.
93
+ */
94
+ export async function fetchCodexRateLimitResetCredits(
95
+ a: CodexAuthHeaders,
96
+ fetchImpl: CodexFetch = fetch,
97
+ timeoutMs = RESET_CREDIT_DETAILS_TIMEOUT_MS,
98
+ ): Promise<
99
+ | { ok: true; status: number; details: CodexRateLimitResetCreditsDetails }
100
+ | { ok: false; status: number; reason: ResetCreditFetchFailureReason }
101
+ > {
102
+ const fetched = await runBoundedCodexOperation(async (signal) => {
103
+ const res = await fetchImpl(`${CODEX_WHAM_BASE}/wham/rate-limit-reset-credits`, {
104
+ method: "GET",
105
+ headers: subscriptionHeaders(a),
106
+ signal,
107
+ });
108
+ if (!res.ok) {
109
+ // Drain the body without retaining/logging it. Provider error bodies may
110
+ // contain account-specific details and are not part of this contract.
111
+ await res.arrayBuffer().catch(() => undefined);
112
+ return { ok: false as const, status: res.status, reason: "http_error" as const };
113
+ }
114
+ const details = parseCodexRateLimitResetCreditsDetails(await res.json().catch(() => null));
115
+ return details
116
+ ? { ok: true as const, status: res.status, details }
117
+ : { ok: false as const, status: res.status, reason: "invalid_response" as const };
118
+ }, timeoutMs);
119
+ return fetched.ok ? fetched.value : { ok: false, status: 0, reason: fetched.reason };
120
+ }
121
+
122
+ /**
123
+ * POST /wham/rate-limit-reset-credits/consume with the exact v0.144.6 body.
124
+ * `idempotencyKey` identifies one logical human redemption and MUST be reused
125
+ * by the server on retries. Supplying `creditId` is preferred; omission leaves
126
+ * provider selection in control and is therefore not used by OpenGeni's
127
+ * human-only flow.
128
+ */
129
+ export async function consumeCodexRateLimitResetCredit(
130
+ a: CodexAuthHeaders,
131
+ input: { idempotencyKey: string; creditId?: string | undefined },
132
+ fetchImpl: CodexFetch = fetch,
133
+ timeoutMs = RESET_CREDIT_CONSUME_TIMEOUT_MS,
134
+ ): Promise<
135
+ | { ok: true; status: number; result: CodexRateLimitResetConsumeResponse }
136
+ | {
137
+ ok: false;
138
+ status: number;
139
+ reason: ResetCreditFetchFailureReason | "invalid_request";
140
+ }
141
+ > {
142
+ if (input.idempotencyKey.length === 0 || input.creditId === "") {
143
+ return { ok: false, status: 0, reason: "invalid_request" };
144
+ }
145
+ const fetched = await runBoundedCodexOperation(async (signal) => {
146
+ const res = await fetchImpl(`${CODEX_WHAM_BASE}/wham/rate-limit-reset-credits/consume`, {
147
+ method: "POST",
148
+ headers: {
149
+ ...subscriptionHeaders(a),
150
+ "content-type": "application/json",
151
+ },
152
+ body: JSON.stringify({
153
+ redeem_request_id: input.idempotencyKey,
154
+ ...(input.creditId ? { credit_id: input.creditId } : {}),
155
+ }),
156
+ signal,
157
+ });
158
+ if (!res.ok) {
159
+ await res.arrayBuffer().catch(() => undefined);
160
+ return { ok: false as const, status: res.status, reason: "http_error" as const };
161
+ }
162
+ const result = parseCodexRateLimitResetConsumeResponse(await res.json().catch(() => null));
163
+ return result
164
+ ? { ok: true as const, status: res.status, result }
165
+ : { ok: false as const, status: res.status, reason: "invalid_response" as const };
166
+ }, timeoutMs);
167
+ return fetched.ok ? fetched.value : { ok: false, status: 0, reason: fetched.reason };
59
168
  }
@@ -0,0 +1,43 @@
1
+ export type CodexOperationFailureReason = "network_error" | "timeout";
2
+
3
+ /**
4
+ * Bound the complete provider operation, including response-body consumption.
5
+ *
6
+ * AbortController makes native fetch release its socket, while Promise.race is
7
+ * the backstop for injected/custom fetch implementations that ignore `signal`.
8
+ * The losing operation is rejection-handled and can never become an unhandled
9
+ * promise after the caller has received the timeout result.
10
+ */
11
+ export async function runBoundedCodexOperation<T>(
12
+ operation: (signal: AbortSignal) => Promise<T>,
13
+ timeoutMs: number,
14
+ ): Promise<{ ok: true; value: T } | { ok: false; reason: CodexOperationFailureReason }> {
15
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
16
+ throw new Error("Codex operation timeout must be positive");
17
+ }
18
+
19
+ const controller = new AbortController();
20
+ let timedOut = false;
21
+ let timeout: ReturnType<typeof setTimeout> | undefined;
22
+ const work = operation(controller.signal).then(
23
+ (value) => ({ ok: true as const, value }),
24
+ () => ({
25
+ ok: false as const,
26
+ reason:
27
+ timedOut || controller.signal.aborted ? ("timeout" as const) : ("network_error" as const),
28
+ }),
29
+ );
30
+ const deadline = new Promise<{ ok: false; reason: "timeout" }>((resolve) => {
31
+ timeout = setTimeout(() => {
32
+ timedOut = true;
33
+ controller.abort();
34
+ resolve({ ok: false, reason: "timeout" });
35
+ }, timeoutMs);
36
+ });
37
+
38
+ try {
39
+ return await Promise.race([work, deadline]);
40
+ } finally {
41
+ if (timeout) clearTimeout(timeout);
42
+ }
43
+ }
package/src/constants.ts CHANGED
@@ -31,7 +31,7 @@ export const CODEX_MODEL_ID_PREFIX = "codex/";
31
31
  export const CODEX_FALLBACK_MODEL_SLUGS = ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"] as const;
32
32
 
33
33
  // Live Codex model-catalog values for every exposed gpt-5.6 subscription slug.
34
- // Verified 2026-07-17 against Codex CLI 0.144.5's freshly fetched
34
+ // Verified 2026-07-18 against Codex CLI 0.144.6's freshly fetched
35
35
  // ~/.codex/models_cache.json and the matching openai/codex core derivations:
36
36
  // raw context window = 272,000
37
37
  // effective input window (95%) = 258,400
@@ -52,11 +52,29 @@ export const CODEX_MODEL_AUTO_COMPACT_TOKEN_LIMIT = Math.floor(
52
52
  // 2026-07-09: 0.142.4 filtered every GPT-5.6 slug out of GET /models, while the
53
53
  // official Codex 0.144.0+ releases return all three exact slugs above. Keep
54
54
  // this pinned to the latest stable Codex release we have verified end-to-end.
55
- export const CODEX_CLIENT_VERSION = "0.144.5";
55
+ export const CODEX_CLIENT_VERSION = "0.144.6";
56
56
 
57
57
  export const CODEX_REFRESH_WINDOW_MS = 5 * 60 * 1000; // proactive refresh when within 5 min of exp (spec §1.1)
58
58
  export const CODEX_REFRESH_FALLBACK_MS = 8 * 24 * 60 * 60 * 1000; // 8 days when exp is unparseable
59
59
 
60
+ // Codex Responses transport deadlines. The OpenAI SDK's own timeout only covers
61
+ // the wait for response headers and erases the underlying timeout class into the
62
+ // bare `Request timed out.` error. Keep the provider-specific budgets here so
63
+ // the transport can enforce and durably report them without enabling the SDK's
64
+ // blind request replay.
65
+ export const CODEX_RESPONSE_HEADERS_TIMEOUT_MS = 4 * 60_000;
66
+ export const CODEX_RESPONSE_STREAM_IDLE_TIMEOUT_MS = 5 * 60_000;
67
+ export const CODEX_RESPONSE_WHOLE_TIMEOUT_MS = 30 * 60_000;
68
+ // Kept as a compatibility-shaped policy field, but automatic replay is disabled
69
+ // until a provider-specific operation receipt can prove non-acceptance or resume
70
+ // the same operation identity. An absent response does not prove that the
71
+ // provider never accepted the request.
72
+ export const CODEX_RESPONSE_NO_BYTE_RETRIES = 0;
73
+ export const CODEX_RESPONSE_RETRY_BACKOFF_MS = 1_000;
74
+ // Must exceed the transport-owned whole-response deadline. This SDK guard is a
75
+ // last-resort envelope; the inner transport emits the typed/durable failure.
76
+ export const CODEX_RESPONSE_SDK_OUTER_TIMEOUT_MS = 35 * 60_000;
77
+
60
78
  // ── Apps / connectors MCP (spec §1.10, §E) ───────────────────────────────────
61
79
  // One server-side MCP exposes ALL the user's ChatGPT/Codex connectors
62
80
  // (gmail/github/linear/slack/sentry/drive/calendar/…). Streamable HTTP, always.