@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/dist/{chunk-K47573CJ.js → chunk-YGKQDUY7.js} +14 -2
- package/dist/{chunk-K47573CJ.js.map → chunk-YGKQDUY7.js.map} +1 -1
- package/dist/constants.d.ts +8 -2
- package/dist/constants.js +13 -1
- package/dist/index.d.ts +231 -5
- package/dist/index.js +1402 -91
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/api-client.ts +131 -22
- package/src/bounded-operation.ts +43 -0
- package/src/constants.ts +20 -2
- package/src/fetch.ts +866 -49
- package/src/index.ts +3 -0
- package/src/model-output-truncation.ts +590 -0
- package/src/oversized-image-card.ts +18 -0
- package/src/refresh.ts +21 -10
- package/src/request-context.ts +38 -0
- package/src/reset-credits.ts +157 -0
- package/src/response-timeout.ts +159 -0
- package/src/usage-normalize.ts +20 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/codex",
|
|
3
|
-
"version": "0.2.
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
method: "GET",
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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-
|
|
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.
|
|
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.
|