@agentionai/agents 1.12.0 → 1.14.0
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/agents/Agent.d.ts +1 -1
- package/dist/agents/AgentConfig.d.ts +34 -0
- package/dist/agents/AgentEvent.d.ts +6 -0
- package/dist/agents/AgentEvent.js +6 -0
- package/dist/agents/BaseAgent.d.ts +18 -0
- package/dist/agents/BaseAgent.js +2 -0
- package/dist/agents/openai/CodexAgent.d.ts +169 -0
- package/dist/agents/openai/CodexAgent.js +222 -0
- package/dist/agents/openai/OpenAiAgent.d.ts +133 -6
- package/dist/agents/openai/OpenAiAgent.js +219 -17
- package/dist/agents/openai/codex-auth.d.ts +166 -0
- package/dist/agents/openai/codex-auth.js +264 -0
- package/dist/agents/openai/codex-usage.d.ts +88 -0
- package/dist/agents/openai/codex-usage.js +127 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +15 -1
- package/dist/openai.d.ts +6 -1
- package/dist/openai.js +19 -1
- package/package.json +1 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth against a ChatGPT subscription, as used by OpenAI's Codex CLI.
|
|
3
|
+
*
|
|
4
|
+
* This is a different product surface from the platform API: the credentials are
|
|
5
|
+
* a ChatGPT login rather than a `sk-...` platform key, and requests are billed
|
|
6
|
+
* against the subscription instead of an API account. The endpoint differs too —
|
|
7
|
+
* see {@link CODEX_BASE_URL}.
|
|
8
|
+
*
|
|
9
|
+
* None of it is a documented public API. The values here were cross-checked
|
|
10
|
+
* against the Codex CLI's own behaviour and several independent
|
|
11
|
+
* reimplementations, but OpenAI can change them without notice.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Base URL for the ChatGPT-backed Codex Responses API.
|
|
15
|
+
*
|
|
16
|
+
* The SDK appends `/responses`, giving
|
|
17
|
+
* `https://chatgpt.com/backend-api/codex/responses`.
|
|
18
|
+
*/
|
|
19
|
+
export declare const CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex";
|
|
20
|
+
/** Public OAuth client id the Codex CLI uses. Not a secret. */
|
|
21
|
+
export declare const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
22
|
+
/** Token endpoint used to exchange a refresh token for a fresh access token. */
|
|
23
|
+
export declare const CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
24
|
+
/**
|
|
25
|
+
* Default `originator` header value.
|
|
26
|
+
*
|
|
27
|
+
* OpenAI gates parts of the model catalog on this, so an unrecognised value can
|
|
28
|
+
* quietly change which models an account may reach.
|
|
29
|
+
*/
|
|
30
|
+
export declare const CODEX_ORIGINATOR = "codex_cli_rs";
|
|
31
|
+
/**
|
|
32
|
+
* `client_version` for the Codex models endpoint, which 400s without one.
|
|
33
|
+
*
|
|
34
|
+
* Each model also carries a `minimal_client_version`; the backend hides models
|
|
35
|
+
* newer than the version claimed here, so an old value quietly shortens the
|
|
36
|
+
* list rather than erroring.
|
|
37
|
+
*/
|
|
38
|
+
export declare const CODEX_CLIENT_VERSION = "0.153.4";
|
|
39
|
+
/**
|
|
40
|
+
* One entry from the Codex `/models` response.
|
|
41
|
+
*
|
|
42
|
+
* Nothing like the platform API's `/v1/models` — richer, and shaped for the
|
|
43
|
+
* Codex client. Only the fields worth relying on are named; the rest come
|
|
44
|
+
* through on `ModelInfo.raw`.
|
|
45
|
+
*/
|
|
46
|
+
export interface CodexModelCard {
|
|
47
|
+
slug: string;
|
|
48
|
+
display_name?: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
/** Default context window for this account's plan. */
|
|
51
|
+
context_window?: number;
|
|
52
|
+
/** Largest window the model can be driven at. */
|
|
53
|
+
max_context_window?: number;
|
|
54
|
+
input_modalities?: string[];
|
|
55
|
+
supported_reasoning_levels?: {
|
|
56
|
+
effort: string;
|
|
57
|
+
description?: string;
|
|
58
|
+
}[];
|
|
59
|
+
default_reasoning_level?: string;
|
|
60
|
+
/** Subscription plans that may select this model. */
|
|
61
|
+
available_in_plans?: string[];
|
|
62
|
+
/** `"list"` for models meant to be shown in a picker. */
|
|
63
|
+
visibility?: string;
|
|
64
|
+
supported_in_api?: boolean;
|
|
65
|
+
minimal_client_version?: string;
|
|
66
|
+
supports_parallel_tool_calls?: boolean;
|
|
67
|
+
[key: string]: unknown;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Credentials for the ChatGPT/Codex backend.
|
|
71
|
+
*/
|
|
72
|
+
export interface CodexCredentials {
|
|
73
|
+
/** Bearer token sent as `Authorization`. */
|
|
74
|
+
accessToken: string;
|
|
75
|
+
/** Used to mint a new access token once the current one expires. */
|
|
76
|
+
refreshToken?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Workspace/account the request is billed to, sent as the
|
|
79
|
+
* `chatgpt-account-id` header. Read from `auth.json`, or decoded from the
|
|
80
|
+
* `id_token` when absent.
|
|
81
|
+
*/
|
|
82
|
+
accountId?: string;
|
|
83
|
+
/** Account e-mail, when the `id_token` carried one. Informational. */
|
|
84
|
+
email?: string;
|
|
85
|
+
/** Subscription tier (`plus`, `pro`, …), when present. Informational. */
|
|
86
|
+
planType?: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Decode a JWT's payload without verifying its signature.
|
|
90
|
+
*
|
|
91
|
+
* Verification is the token endpoint's job — we are only reading claims out of a
|
|
92
|
+
* token we were just handed over TLS, never making a trust decision on it.
|
|
93
|
+
* Returns `undefined` for anything that does not parse, so a malformed or
|
|
94
|
+
* opaque token degrades to "no claims" rather than throwing.
|
|
95
|
+
*/
|
|
96
|
+
export declare function decodeJwtClaims<T = Record<string, unknown>>(token: string): T | undefined;
|
|
97
|
+
/**
|
|
98
|
+
* Seconds-since-epoch expiry of a JWT, or `undefined` if it has no `exp`.
|
|
99
|
+
*/
|
|
100
|
+
export declare function jwtExpiry(token: string): number | undefined;
|
|
101
|
+
/** Default location of Codex's credential file. */
|
|
102
|
+
export declare function codexAuthFilePath(codexHome?: string): string;
|
|
103
|
+
/**
|
|
104
|
+
* Read the credentials the Codex CLI stored at `$CODEX_HOME/auth.json`
|
|
105
|
+
* (`~/.codex/auth.json` by default).
|
|
106
|
+
*
|
|
107
|
+
* Sign in with `codex login` first — this only reads what that wrote, it does
|
|
108
|
+
* not run the OAuth flow itself.
|
|
109
|
+
*
|
|
110
|
+
* @throws if the file is missing, unreadable, not JSON, or holds no access token.
|
|
111
|
+
*/
|
|
112
|
+
export declare function loadCodexCredentials(codexHome?: string): Promise<CodexCredentials>;
|
|
113
|
+
/**
|
|
114
|
+
* Exchange a refresh token for a fresh access token.
|
|
115
|
+
*
|
|
116
|
+
* The returned credentials carry the new `refresh_token` when the server
|
|
117
|
+
* rotated it, and the previous one otherwise.
|
|
118
|
+
*/
|
|
119
|
+
export declare function refreshCodexCredentials(refreshToken: string, options?: {
|
|
120
|
+
clientId?: string;
|
|
121
|
+
tokenUrl?: string;
|
|
122
|
+
signal?: AbortSignal;
|
|
123
|
+
}): Promise<CodexCredentials>;
|
|
124
|
+
/** Options for {@link createCodexTokenProvider}. */
|
|
125
|
+
export interface CodexTokenProviderOptions {
|
|
126
|
+
/** OAuth client id. Defaults to {@link CODEX_CLIENT_ID}. */
|
|
127
|
+
clientId?: string;
|
|
128
|
+
/** Token endpoint. Defaults to {@link CODEX_TOKEN_URL}. */
|
|
129
|
+
tokenUrl?: string;
|
|
130
|
+
/**
|
|
131
|
+
* Refresh this many seconds before the access token actually expires, so a
|
|
132
|
+
* request is never sent with a token that dies in flight.
|
|
133
|
+
*
|
|
134
|
+
* @default 300
|
|
135
|
+
*/
|
|
136
|
+
refreshSkewSeconds?: number;
|
|
137
|
+
/**
|
|
138
|
+
* Called after every successful refresh, e.g. to persist the rotated refresh
|
|
139
|
+
* token. Errors thrown here are ignored — a failed write must not fail the
|
|
140
|
+
* request the token was minted for.
|
|
141
|
+
*/
|
|
142
|
+
onRefresh?: (credentials: CodexCredentials) => void | Promise<void>;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* A token source that hands out an access token and silently refreshes it.
|
|
146
|
+
*
|
|
147
|
+
* The `getToken` function is shaped for the OpenAI SDK's `apiKey` option, which
|
|
148
|
+
* accepts an async function and calls it before *every* request — so a
|
|
149
|
+
* long-running agent keeps working past the ~1h life of an access token without
|
|
150
|
+
* anyone reaching for the credential file again.
|
|
151
|
+
*/
|
|
152
|
+
export interface CodexTokenProvider {
|
|
153
|
+
/** Current access token, refreshed on demand. Pass as the SDK's `apiKey`. */
|
|
154
|
+
getToken: () => Promise<string>;
|
|
155
|
+
/** Latest known credentials, including `accountId`. */
|
|
156
|
+
current: () => CodexCredentials;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Wrap credentials in a self-refreshing token provider.
|
|
160
|
+
*
|
|
161
|
+
* Refreshes lazily — only when a token is actually asked for and the current
|
|
162
|
+
* one is within `refreshSkewSeconds` of expiry. Concurrent callers share a
|
|
163
|
+
* single in-flight refresh rather than each starting their own.
|
|
164
|
+
*/
|
|
165
|
+
export declare function createCodexTokenProvider(credentials: CodexCredentials, options?: CodexTokenProviderOptions): CodexTokenProvider;
|
|
166
|
+
//# sourceMappingURL=codex-auth.d.ts.map
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CODEX_CLIENT_VERSION = exports.CODEX_ORIGINATOR = exports.CODEX_TOKEN_URL = exports.CODEX_CLIENT_ID = exports.CODEX_BASE_URL = void 0;
|
|
37
|
+
exports.decodeJwtClaims = decodeJwtClaims;
|
|
38
|
+
exports.jwtExpiry = jwtExpiry;
|
|
39
|
+
exports.codexAuthFilePath = codexAuthFilePath;
|
|
40
|
+
exports.loadCodexCredentials = loadCodexCredentials;
|
|
41
|
+
exports.refreshCodexCredentials = refreshCodexCredentials;
|
|
42
|
+
exports.createCodexTokenProvider = createCodexTokenProvider;
|
|
43
|
+
const fs_1 = require("fs");
|
|
44
|
+
const os = __importStar(require("os"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
/**
|
|
47
|
+
* OAuth against a ChatGPT subscription, as used by OpenAI's Codex CLI.
|
|
48
|
+
*
|
|
49
|
+
* This is a different product surface from the platform API: the credentials are
|
|
50
|
+
* a ChatGPT login rather than a `sk-...` platform key, and requests are billed
|
|
51
|
+
* against the subscription instead of an API account. The endpoint differs too —
|
|
52
|
+
* see {@link CODEX_BASE_URL}.
|
|
53
|
+
*
|
|
54
|
+
* None of it is a documented public API. The values here were cross-checked
|
|
55
|
+
* against the Codex CLI's own behaviour and several independent
|
|
56
|
+
* reimplementations, but OpenAI can change them without notice.
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* Base URL for the ChatGPT-backed Codex Responses API.
|
|
60
|
+
*
|
|
61
|
+
* The SDK appends `/responses`, giving
|
|
62
|
+
* `https://chatgpt.com/backend-api/codex/responses`.
|
|
63
|
+
*/
|
|
64
|
+
exports.CODEX_BASE_URL = "https://chatgpt.com/backend-api/codex";
|
|
65
|
+
/** Public OAuth client id the Codex CLI uses. Not a secret. */
|
|
66
|
+
exports.CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
67
|
+
/** Token endpoint used to exchange a refresh token for a fresh access token. */
|
|
68
|
+
exports.CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
69
|
+
/**
|
|
70
|
+
* Default `originator` header value.
|
|
71
|
+
*
|
|
72
|
+
* OpenAI gates parts of the model catalog on this, so an unrecognised value can
|
|
73
|
+
* quietly change which models an account may reach.
|
|
74
|
+
*/
|
|
75
|
+
exports.CODEX_ORIGINATOR = "codex_cli_rs";
|
|
76
|
+
/**
|
|
77
|
+
* `client_version` for the Codex models endpoint, which 400s without one.
|
|
78
|
+
*
|
|
79
|
+
* Each model also carries a `minimal_client_version`; the backend hides models
|
|
80
|
+
* newer than the version claimed here, so an old value quietly shortens the
|
|
81
|
+
* list rather than erroring.
|
|
82
|
+
*/
|
|
83
|
+
exports.CODEX_CLIENT_VERSION = "0.153.4";
|
|
84
|
+
/**
|
|
85
|
+
* Decode a JWT's payload without verifying its signature.
|
|
86
|
+
*
|
|
87
|
+
* Verification is the token endpoint's job — we are only reading claims out of a
|
|
88
|
+
* token we were just handed over TLS, never making a trust decision on it.
|
|
89
|
+
* Returns `undefined` for anything that does not parse, so a malformed or
|
|
90
|
+
* opaque token degrades to "no claims" rather than throwing.
|
|
91
|
+
*/
|
|
92
|
+
function decodeJwtClaims(token) {
|
|
93
|
+
const payload = token.split(".")[1];
|
|
94
|
+
if (!payload)
|
|
95
|
+
return undefined;
|
|
96
|
+
try {
|
|
97
|
+
const json = Buffer.from(payload, "base64url").toString("utf8");
|
|
98
|
+
const claims = JSON.parse(json);
|
|
99
|
+
return typeof claims === "object" && claims !== null
|
|
100
|
+
? claims
|
|
101
|
+
: undefined;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Seconds-since-epoch expiry of a JWT, or `undefined` if it has no `exp`.
|
|
109
|
+
*/
|
|
110
|
+
function jwtExpiry(token) {
|
|
111
|
+
const exp = decodeJwtClaims(token)?.exp;
|
|
112
|
+
return typeof exp === "number" ? exp : undefined;
|
|
113
|
+
}
|
|
114
|
+
/** Default location of Codex's credential file. */
|
|
115
|
+
function codexAuthFilePath(codexHome) {
|
|
116
|
+
const home = codexHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
|
|
117
|
+
return path.join(home, "auth.json");
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Read the credentials the Codex CLI stored at `$CODEX_HOME/auth.json`
|
|
121
|
+
* (`~/.codex/auth.json` by default).
|
|
122
|
+
*
|
|
123
|
+
* Sign in with `codex login` first — this only reads what that wrote, it does
|
|
124
|
+
* not run the OAuth flow itself.
|
|
125
|
+
*
|
|
126
|
+
* @throws if the file is missing, unreadable, not JSON, or holds no access token.
|
|
127
|
+
*/
|
|
128
|
+
async function loadCodexCredentials(codexHome) {
|
|
129
|
+
const file = codexAuthFilePath(codexHome);
|
|
130
|
+
let raw;
|
|
131
|
+
try {
|
|
132
|
+
raw = await fs_1.promises.readFile(file, "utf8");
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
const reason = error?.code === "ENOENT"
|
|
136
|
+
? "no such file — run `codex login` to sign in with your ChatGPT account"
|
|
137
|
+
: error instanceof Error
|
|
138
|
+
? error.message
|
|
139
|
+
: "unknown error";
|
|
140
|
+
throw new Error(`Could not read Codex credentials from ${file}: ${reason}`);
|
|
141
|
+
}
|
|
142
|
+
let parsed;
|
|
143
|
+
try {
|
|
144
|
+
parsed = JSON.parse(raw);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
throw new Error(`Codex credentials at ${file} are not valid JSON`);
|
|
148
|
+
}
|
|
149
|
+
const tokens = parsed.tokens;
|
|
150
|
+
if (!tokens?.access_token) {
|
|
151
|
+
throw new Error(`Codex credentials at ${file} contain no OAuth access token` +
|
|
152
|
+
(parsed.OPENAI_API_KEY
|
|
153
|
+
? " — that file holds a platform API key instead, which belongs in `apiKey` with the default `authType: \"apiKey\"`"
|
|
154
|
+
: " — run `codex login` to sign in with your ChatGPT account"));
|
|
155
|
+
}
|
|
156
|
+
return credentialsFromTokens(tokens);
|
|
157
|
+
}
|
|
158
|
+
/** Build {@link CodexCredentials} from an `auth.json` `tokens` object. */
|
|
159
|
+
function credentialsFromTokens(tokens) {
|
|
160
|
+
const claims = tokens.id_token
|
|
161
|
+
? decodeJwtClaims(tokens.id_token)
|
|
162
|
+
: undefined;
|
|
163
|
+
const auth = claims?.["https://api.openai.com/auth"];
|
|
164
|
+
return {
|
|
165
|
+
accessToken: tokens.access_token,
|
|
166
|
+
refreshToken: tokens.refresh_token,
|
|
167
|
+
// `auth.json` usually carries `account_id`, but not always; the same value
|
|
168
|
+
// is a claim on the id_token, so fall back to that before giving up.
|
|
169
|
+
accountId: tokens.account_id ?? auth?.chatgpt_account_id,
|
|
170
|
+
email: claims?.email ?? claims?.["https://api.openai.com/profile"]?.email,
|
|
171
|
+
planType: auth?.chatgpt_plan_type,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Exchange a refresh token for a fresh access token.
|
|
176
|
+
*
|
|
177
|
+
* The returned credentials carry the new `refresh_token` when the server
|
|
178
|
+
* rotated it, and the previous one otherwise.
|
|
179
|
+
*/
|
|
180
|
+
async function refreshCodexCredentials(refreshToken, options = {}) {
|
|
181
|
+
const res = await fetch(options.tokenUrl ?? exports.CODEX_TOKEN_URL, {
|
|
182
|
+
method: "POST",
|
|
183
|
+
headers: { "Content-Type": "application/json" },
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
grant_type: "refresh_token",
|
|
186
|
+
refresh_token: refreshToken,
|
|
187
|
+
client_id: options.clientId ?? exports.CODEX_CLIENT_ID,
|
|
188
|
+
}),
|
|
189
|
+
signal: options.signal,
|
|
190
|
+
});
|
|
191
|
+
if (!res.ok) {
|
|
192
|
+
const body = await res.text().catch(() => "");
|
|
193
|
+
throw new Error(`Codex token refresh failed (${res.status} ${res.statusText})${body ? `: ${body.slice(0, 500)}` : ""}`);
|
|
194
|
+
}
|
|
195
|
+
const data = (await res.json());
|
|
196
|
+
if (!data.access_token) {
|
|
197
|
+
throw new Error("Codex token refresh returned no access_token");
|
|
198
|
+
}
|
|
199
|
+
return credentialsFromTokens({
|
|
200
|
+
access_token: data.access_token,
|
|
201
|
+
// The endpoint only returns a refresh token when it rotates one; reuse the
|
|
202
|
+
// current one otherwise, or the next refresh has nothing to present.
|
|
203
|
+
refresh_token: data.refresh_token ?? refreshToken,
|
|
204
|
+
id_token: data.id_token,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Wrap credentials in a self-refreshing token provider.
|
|
209
|
+
*
|
|
210
|
+
* Refreshes lazily — only when a token is actually asked for and the current
|
|
211
|
+
* one is within `refreshSkewSeconds` of expiry. Concurrent callers share a
|
|
212
|
+
* single in-flight refresh rather than each starting their own.
|
|
213
|
+
*/
|
|
214
|
+
function createCodexTokenProvider(credentials, options = {}) {
|
|
215
|
+
const skew = options.refreshSkewSeconds ?? 300;
|
|
216
|
+
let current = credentials;
|
|
217
|
+
let expiresAt = jwtExpiry(credentials.accessToken);
|
|
218
|
+
let inFlight;
|
|
219
|
+
const isFresh = () => {
|
|
220
|
+
// An opaque token with no readable `exp` is assumed good: refreshing on
|
|
221
|
+
// every call would be worse than letting a 401 surface.
|
|
222
|
+
if (expiresAt === undefined)
|
|
223
|
+
return true;
|
|
224
|
+
return Date.now() / 1000 < expiresAt - skew;
|
|
225
|
+
};
|
|
226
|
+
const refresh = async () => {
|
|
227
|
+
if (!current.refreshToken) {
|
|
228
|
+
throw new Error("Codex access token has expired and no refresh token is available — run `codex login` again");
|
|
229
|
+
}
|
|
230
|
+
const next = await refreshCodexCredentials(current.refreshToken, {
|
|
231
|
+
clientId: options.clientId,
|
|
232
|
+
tokenUrl: options.tokenUrl,
|
|
233
|
+
});
|
|
234
|
+
current = {
|
|
235
|
+
...next,
|
|
236
|
+
// A refresh response carries no id_token in some cases, which would drop
|
|
237
|
+
// the account id the `chatgpt-account-id` header needs.
|
|
238
|
+
accountId: next.accountId ?? current.accountId,
|
|
239
|
+
email: next.email ?? current.email,
|
|
240
|
+
planType: next.planType ?? current.planType,
|
|
241
|
+
};
|
|
242
|
+
expiresAt = jwtExpiry(current.accessToken);
|
|
243
|
+
try {
|
|
244
|
+
await options.onRefresh?.(current);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// Persisting is best-effort; the token in hand is still valid.
|
|
248
|
+
}
|
|
249
|
+
return current.accessToken;
|
|
250
|
+
};
|
|
251
|
+
return {
|
|
252
|
+
getToken: async () => {
|
|
253
|
+
if (isFresh())
|
|
254
|
+
return current.accessToken;
|
|
255
|
+
// Collapse concurrent refreshes: the second caller awaits the first.
|
|
256
|
+
inFlight ?? (inFlight = refresh().finally(() => {
|
|
257
|
+
inFlight = undefined;
|
|
258
|
+
}));
|
|
259
|
+
return inFlight;
|
|
260
|
+
},
|
|
261
|
+
current: () => current,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=codex-auth.js.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quota accounting for the ChatGPT-backed Codex backend.
|
|
3
|
+
*
|
|
4
|
+
* A ChatGPT subscription is not billed per request, so nothing on this path
|
|
5
|
+
* reports a dollar cost — `TokenUsage.cost_usd` stays undefined here, as it
|
|
6
|
+
* does on every provider that does not price a response itself. What a
|
|
7
|
+
* subscription spends instead is *plan allowance*, and the backend reports that
|
|
8
|
+
* on every `/responses` call as a set of `x-codex-*` headers: two rolling
|
|
9
|
+
* windows (a 5-hour "primary" and a weekly "secondary"), the plan and limit
|
|
10
|
+
* tier in force, and the pay-as-you-go credit balance that takes over once the
|
|
11
|
+
* windows are exhausted.
|
|
12
|
+
*
|
|
13
|
+
* These headers are the only source: there is no usage endpoint (`/usage`,
|
|
14
|
+
* `/rate_limits` and `/limits` all answer `403`), and `/models` returns none of
|
|
15
|
+
* them — so quota state can only be refreshed by making a real call. Values
|
|
16
|
+
* observed live against `chatgpt.com/backend-api/codex` on 2026-09-10; like the
|
|
17
|
+
* rest of that surface they are undocumented and may change without notice,
|
|
18
|
+
* which is why every field here is optional and an unparseable value is dropped
|
|
19
|
+
* rather than guessed at.
|
|
20
|
+
*/
|
|
21
|
+
/** One rolling usage window, as the backend reports it. */
|
|
22
|
+
export interface CodexRateLimitWindow {
|
|
23
|
+
/**
|
|
24
|
+
* Percentage of the window's allowance already consumed, `0`–`100`. Requests
|
|
25
|
+
* start failing once this reaches 100 and the other window has nothing left
|
|
26
|
+
* either.
|
|
27
|
+
*/
|
|
28
|
+
usedPercent: number;
|
|
29
|
+
/**
|
|
30
|
+
* Length of the rolling window in minutes — `300` (5 hours) for the primary
|
|
31
|
+
* window and `10080` (7 days) for the secondary, on the plans seen so far.
|
|
32
|
+
*/
|
|
33
|
+
windowMinutes?: number;
|
|
34
|
+
/** Seconds until the window rolls over and the allowance is restored. */
|
|
35
|
+
resetAfterSeconds?: number;
|
|
36
|
+
/** Wall-clock time the window rolls over. */
|
|
37
|
+
resetAt?: Date;
|
|
38
|
+
}
|
|
39
|
+
/** Pay-as-you-go credit balance, used once the plan windows are exhausted. */
|
|
40
|
+
export interface CodexCredits {
|
|
41
|
+
/** Remaining credits. `0` on an account that has never bought any. */
|
|
42
|
+
balance?: number;
|
|
43
|
+
/** Whether any credits are available to spend. */
|
|
44
|
+
hasCredits?: boolean;
|
|
45
|
+
/** Whether the account's credits are uncapped. */
|
|
46
|
+
unlimited?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* What one Codex response said about the subscription's remaining allowance —
|
|
50
|
+
* the closest thing this backend has to a cost figure.
|
|
51
|
+
*/
|
|
52
|
+
export interface CodexUsageLimits {
|
|
53
|
+
/** Short rolling window; `300` minutes (5 hours) on the plans seen so far. */
|
|
54
|
+
primary?: CodexRateLimitWindow;
|
|
55
|
+
/** Long rolling window; `10080` minutes (7 days) on those same plans. */
|
|
56
|
+
secondary?: CodexRateLimitWindow;
|
|
57
|
+
/** Subscription tier the request was billed against, e.g. `"plus"`. */
|
|
58
|
+
planType?: string;
|
|
59
|
+
/** Limit tier in force for this request, e.g. `"premium"`. */
|
|
60
|
+
activeLimit?: string;
|
|
61
|
+
/** Credit balance backing the account once the windows run dry. */
|
|
62
|
+
credits?: CodexCredits;
|
|
63
|
+
/**
|
|
64
|
+
* How far the primary window may run past the secondary window's pace, as a
|
|
65
|
+
* percentage. `0` where the backend imposes no such allowance.
|
|
66
|
+
*/
|
|
67
|
+
primaryOverSecondaryLimitPercent?: number;
|
|
68
|
+
/** When these values were received. */
|
|
69
|
+
at: Date;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Read the quota state out of a Codex response's headers.
|
|
73
|
+
*
|
|
74
|
+
* @returns the limits, or `undefined` when the response carried none — which is
|
|
75
|
+
* every response that is not a `/responses` call, including `/models`
|
|
76
|
+
* and anything Cloudflare answered on the backend's behalf.
|
|
77
|
+
*/
|
|
78
|
+
export declare function parseCodexUsageLimits(headers: Headers): CodexUsageLimits | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* `fetch` wrapper that hands every response's headers to `onHeaders` before
|
|
81
|
+
* returning it untouched.
|
|
82
|
+
*
|
|
83
|
+
* The body is never read here — the SDK still consumes the stream itself — so
|
|
84
|
+
* this is safe to stack under `wrapErrorBodyFetch`. A throwing observer
|
|
85
|
+
* is swallowed: quota bookkeeping must never be able to fail a request.
|
|
86
|
+
*/
|
|
87
|
+
export declare function observeHeadersFetch(onHeaders: (headers: Headers) => void, baseFetch?: typeof fetch): typeof fetch;
|
|
88
|
+
//# sourceMappingURL=codex-usage.d.ts.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Quota accounting for the ChatGPT-backed Codex backend.
|
|
4
|
+
*
|
|
5
|
+
* A ChatGPT subscription is not billed per request, so nothing on this path
|
|
6
|
+
* reports a dollar cost — `TokenUsage.cost_usd` stays undefined here, as it
|
|
7
|
+
* does on every provider that does not price a response itself. What a
|
|
8
|
+
* subscription spends instead is *plan allowance*, and the backend reports that
|
|
9
|
+
* on every `/responses` call as a set of `x-codex-*` headers: two rolling
|
|
10
|
+
* windows (a 5-hour "primary" and a weekly "secondary"), the plan and limit
|
|
11
|
+
* tier in force, and the pay-as-you-go credit balance that takes over once the
|
|
12
|
+
* windows are exhausted.
|
|
13
|
+
*
|
|
14
|
+
* These headers are the only source: there is no usage endpoint (`/usage`,
|
|
15
|
+
* `/rate_limits` and `/limits` all answer `403`), and `/models` returns none of
|
|
16
|
+
* them — so quota state can only be refreshed by making a real call. Values
|
|
17
|
+
* observed live against `chatgpt.com/backend-api/codex` on 2026-09-10; like the
|
|
18
|
+
* rest of that surface they are undocumented and may change without notice,
|
|
19
|
+
* which is why every field here is optional and an unparseable value is dropped
|
|
20
|
+
* rather than guessed at.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.parseCodexUsageLimits = parseCodexUsageLimits;
|
|
24
|
+
exports.observeHeadersFetch = observeHeadersFetch;
|
|
25
|
+
/** Parse a header that should hold a number, dropping anything that does not. */
|
|
26
|
+
function num(headers, name) {
|
|
27
|
+
const raw = headers.get(name);
|
|
28
|
+
if (raw === null || raw.trim() === "")
|
|
29
|
+
return undefined;
|
|
30
|
+
const value = Number(raw);
|
|
31
|
+
return Number.isFinite(value) ? value : undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse a header holding a boolean. The backend writes these Python-style
|
|
35
|
+
* (`True` / `False`), so match case-insensitively and accept the JSON spelling
|
|
36
|
+
* too in case that ever changes.
|
|
37
|
+
*/
|
|
38
|
+
function bool(headers, name) {
|
|
39
|
+
const raw = headers.get(name)?.trim().toLowerCase();
|
|
40
|
+
if (raw === "true")
|
|
41
|
+
return true;
|
|
42
|
+
if (raw === "false")
|
|
43
|
+
return false;
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
/** Parse a `-reset-at` header, which carries seconds since the epoch. */
|
|
47
|
+
function resetAt(headers, name) {
|
|
48
|
+
const seconds = num(headers, name);
|
|
49
|
+
return seconds === undefined ? undefined : new Date(seconds * 1000);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Parse one rolling window's headers.
|
|
53
|
+
*
|
|
54
|
+
* Returns `undefined` unless `used-percent` is present: without it there is no
|
|
55
|
+
* window to speak of, only a reset time for one that was never reported.
|
|
56
|
+
*/
|
|
57
|
+
function window(headers, prefix) {
|
|
58
|
+
const usedPercent = num(headers, `x-codex-${prefix}-used-percent`);
|
|
59
|
+
if (usedPercent === undefined)
|
|
60
|
+
return undefined;
|
|
61
|
+
return {
|
|
62
|
+
usedPercent,
|
|
63
|
+
windowMinutes: num(headers, `x-codex-${prefix}-window-minutes`),
|
|
64
|
+
resetAfterSeconds: num(headers, `x-codex-${prefix}-reset-after-seconds`),
|
|
65
|
+
resetAt: resetAt(headers, `x-codex-${prefix}-reset-at`),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Read the quota state out of a Codex response's headers.
|
|
70
|
+
*
|
|
71
|
+
* @returns the limits, or `undefined` when the response carried none — which is
|
|
72
|
+
* every response that is not a `/responses` call, including `/models`
|
|
73
|
+
* and anything Cloudflare answered on the backend's behalf.
|
|
74
|
+
*/
|
|
75
|
+
function parseCodexUsageLimits(headers) {
|
|
76
|
+
const primary = window(headers, "primary");
|
|
77
|
+
const secondary = window(headers, "secondary");
|
|
78
|
+
const planType = headers.get("x-codex-plan-type") ?? undefined;
|
|
79
|
+
const activeLimit = headers.get("x-codex-active-limit") ?? undefined;
|
|
80
|
+
const balance = num(headers, "x-codex-credits-balance");
|
|
81
|
+
const hasCredits = bool(headers, "x-codex-credits-has-credits");
|
|
82
|
+
const unlimited = bool(headers, "x-codex-credits-unlimited");
|
|
83
|
+
const primaryOverSecondaryLimitPercent = num(headers, "x-codex-primary-over-secondary-limit-percent");
|
|
84
|
+
const credits = balance === undefined && hasCredits === undefined && unlimited === undefined
|
|
85
|
+
? undefined
|
|
86
|
+
: { balance, hasCredits, unlimited };
|
|
87
|
+
// Nothing recognised: report "no limits seen" rather than a shell of an
|
|
88
|
+
// object timestamped as if it were an answer.
|
|
89
|
+
if (!primary &&
|
|
90
|
+
!secondary &&
|
|
91
|
+
!planType &&
|
|
92
|
+
!activeLimit &&
|
|
93
|
+
!credits &&
|
|
94
|
+
primaryOverSecondaryLimitPercent === undefined) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
primary,
|
|
99
|
+
secondary,
|
|
100
|
+
planType,
|
|
101
|
+
activeLimit,
|
|
102
|
+
credits,
|
|
103
|
+
primaryOverSecondaryLimitPercent,
|
|
104
|
+
at: new Date(),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* `fetch` wrapper that hands every response's headers to `onHeaders` before
|
|
109
|
+
* returning it untouched.
|
|
110
|
+
*
|
|
111
|
+
* The body is never read here — the SDK still consumes the stream itself — so
|
|
112
|
+
* this is safe to stack under `wrapErrorBodyFetch`. A throwing observer
|
|
113
|
+
* is swallowed: quota bookkeeping must never be able to fail a request.
|
|
114
|
+
*/
|
|
115
|
+
function observeHeadersFetch(onHeaders, baseFetch = fetch) {
|
|
116
|
+
return async (input, init) => {
|
|
117
|
+
const res = await baseFetch(input, init);
|
|
118
|
+
try {
|
|
119
|
+
onHeaders(res.headers);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Ignored on purpose: see above.
|
|
123
|
+
}
|
|
124
|
+
return res;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=codex-usage.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export * from "./agents/BaseAgent";
|
|
2
2
|
export * from "./agents/anthropic/ClaudeAgent";
|
|
3
3
|
export { OpenAiAgent } from "./agents/openai/OpenAiAgent";
|
|
4
|
+
export { CODEX_BASE_URL, CODEX_CLIENT_ID, CODEX_ORIGINATOR, CODEX_TOKEN_URL, codexAuthFilePath, createCodexTokenProvider, loadCodexCredentials, refreshCodexCredentials, } from "./agents/openai/codex-auth";
|
|
5
|
+
export type { CodexModelCard, CodexCredentials, CodexTokenProvider, CodexTokenProviderOptions, } from "./agents/openai/codex-auth";
|
|
6
|
+
export { parseCodexUsageLimits, observeHeadersFetch, type CodexUsageLimits, type CodexRateLimitWindow, type CodexCredits, } from "./agents/openai/codex-usage";
|
|
7
|
+
export { CodexAgent } from "./agents/openai/CodexAgent";
|
|
8
|
+
export type { CodexAgentConfig, CodexModel, CodexReasoningEffort, } from "./agents/openai/CodexAgent";
|
|
4
9
|
export { MistralAgent } from "./agents/mistral/MistralAgent";
|
|
5
10
|
export type { MistralModelCard } from "./agents/mistral/MistralAgent";
|
|
6
11
|
export { GeminiAgent, GEMINI_RETIRED_MODELS, } from "./agents/google/GeminiAgent";
|