@mewbleh/purrx 1.0.10 → 1.0.13
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/bin/purrx.js +9 -0
- package/package.json +1 -1
- package/src/api/client.js +42 -19
- package/src/auth/tokens.js +14 -2
package/bin/purrx.js
CHANGED
|
@@ -335,6 +335,15 @@ async function main() {
|
|
|
335
335
|
case "status":
|
|
336
336
|
cmdStatus();
|
|
337
337
|
break;
|
|
338
|
+
case "version":
|
|
339
|
+
case "--version":
|
|
340
|
+
case "-v": {
|
|
341
|
+
const { createRequire } = await import("node:module");
|
|
342
|
+
const require = createRequire(import.meta.url);
|
|
343
|
+
const pkg = require("../package.json");
|
|
344
|
+
console.log(`purrx ${pkg.version}`);
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
338
347
|
case "help":
|
|
339
348
|
case "--help":
|
|
340
349
|
case "-h":
|
package/package.json
CHANGED
package/src/api/client.js
CHANGED
|
@@ -1,29 +1,38 @@
|
|
|
1
1
|
import { API_BASE, CHATGPT_BASE } from "../config.js";
|
|
2
2
|
|
|
3
|
-
// Models that
|
|
4
|
-
// and
|
|
3
|
+
// Models that take the Responses API reasoning fields (gpt-5 family, o-series,
|
|
4
|
+
// and codex variants). The official Codex client always sends a reasoning block
|
|
5
|
+
// for these and requests encrypted reasoning back since store is false.
|
|
5
6
|
function isReasoningModel(model) {
|
|
6
7
|
return /^(gpt-5|o\d|codex)/i.test(model) || /-codex\b/i.test(model);
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
// Builds the request URL and headers for the resolved auth mode.
|
|
11
|
+
//
|
|
12
|
+
// The ChatGPT-codex header set mirrors the official Codex CLI
|
|
13
|
+
// (codex-rs default_client + BearerAuthProvider + build_session_headers):
|
|
14
|
+
// Authorization, ChatGPT-Account-ID, originator, User-Agent,
|
|
15
|
+
// OpenAI-Beta, session-id.
|
|
10
16
|
/**
|
|
11
17
|
* @param {import("../types.js").AuthInfo} authInfo
|
|
18
|
+
* @param {string} [sessionId]
|
|
12
19
|
* @returns {{ url: string, headers: Record<string, string> }}
|
|
13
20
|
*/
|
|
14
|
-
function buildRequest(authInfo) {
|
|
21
|
+
function buildRequest(authInfo, sessionId) {
|
|
15
22
|
if (authInfo.mode === "chatgpt") {
|
|
16
23
|
/** @type {Record<string, string>} */
|
|
17
24
|
const headers = {
|
|
18
25
|
"Content-Type": "application/json",
|
|
19
26
|
Authorization: `Bearer ${authInfo.accessToken}`,
|
|
20
|
-
|
|
27
|
+
"OpenAI-Beta": "responses=experimental",
|
|
21
28
|
originator: "codex_cli_rs",
|
|
22
|
-
"User-Agent": "purrx",
|
|
29
|
+
"User-Agent": "codex_cli_rs/0.0.0 (purrx)",
|
|
23
30
|
};
|
|
24
31
|
if (authInfo.accountId) {
|
|
25
|
-
|
|
32
|
+
// Casing matches the official client (BearerAuthProvider).
|
|
33
|
+
headers["ChatGPT-Account-ID"] = authInfo.accountId;
|
|
26
34
|
}
|
|
35
|
+
if (sessionId) headers["session-id"] = sessionId;
|
|
27
36
|
return { url: `${CHATGPT_BASE}/responses`, headers };
|
|
28
37
|
}
|
|
29
38
|
// API-key mode.
|
|
@@ -47,7 +56,7 @@ function buildRequest(authInfo) {
|
|
|
47
56
|
* @param {import("../types.js").HistoryItem[]} req.input
|
|
48
57
|
* @param {import("../types.js").ToolDefinition[]} [req.tools]
|
|
49
58
|
* @param {string} [req.instructions]
|
|
50
|
-
* @param {string} [req.sessionId] stable id
|
|
59
|
+
* @param {string} [req.sessionId] stable id, sent as session-id + prompt_cache_key
|
|
51
60
|
* @param {{ onText?: (delta: string) => void, onEvent?: (event: any) => void }} [handlers]
|
|
52
61
|
* @returns {Promise<any>}
|
|
53
62
|
*/
|
|
@@ -55,33 +64,44 @@ export async function streamResponse(
|
|
|
55
64
|
{ authInfo, model, input, tools, instructions, sessionId },
|
|
56
65
|
handlers = {}
|
|
57
66
|
) {
|
|
58
|
-
const { url, headers } = buildRequest(authInfo);
|
|
67
|
+
const { url, headers } = buildRequest(authInfo, sessionId);
|
|
59
68
|
|
|
69
|
+
// Request body matching the official Codex ResponsesApiRequest. These fields
|
|
70
|
+
// are always present in the real client; sending a stripped-down body is what
|
|
71
|
+
// triggers the backend's misleading "model is not supported" rejection.
|
|
60
72
|
/** @type {Record<string, any>} */
|
|
61
73
|
const payload = {
|
|
62
74
|
model,
|
|
75
|
+
instructions: instructions || "",
|
|
63
76
|
input,
|
|
64
|
-
|
|
65
|
-
|
|
77
|
+
tool_choice: "auto",
|
|
78
|
+
parallel_tool_calls: false,
|
|
66
79
|
store: false,
|
|
67
|
-
|
|
68
|
-
|
|
80
|
+
stream: true,
|
|
81
|
+
include: [],
|
|
69
82
|
};
|
|
70
|
-
if (instructions) payload.instructions = instructions;
|
|
71
83
|
|
|
72
|
-
// Reasoning models (gpt-5*, o-series, *-codex) require a reasoning block and
|
|
73
|
-
// must request the encrypted reasoning content back since we don't persist
|
|
74
|
-
// responses (store:false). This mirrors the official Codex client and is the
|
|
75
|
-
// shape the ChatGPT/Codex backend expects.
|
|
76
84
|
if (isReasoningModel(model)) {
|
|
77
85
|
payload.reasoning = { effort: "medium", summary: "auto" };
|
|
78
86
|
payload.include = ["reasoning.encrypted_content"];
|
|
79
87
|
}
|
|
80
88
|
|
|
89
|
+
if (sessionId) payload.prompt_cache_key = sessionId;
|
|
90
|
+
|
|
81
91
|
if (tools && tools.length) {
|
|
82
92
|
payload.tools = tools;
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Debug: dump the exact outgoing request when PURRX_DEBUG is set. Auth tokens
|
|
96
|
+
// are redacted. This is the fastest way to diagnose backend rejections.
|
|
97
|
+
if (process.env.PURRX_DEBUG) {
|
|
98
|
+
const safeHeaders = { ...headers };
|
|
99
|
+
if (safeHeaders.Authorization) safeHeaders.Authorization = "Bearer <redacted>";
|
|
100
|
+
console.error(`\n[purrx] POST ${url}`);
|
|
101
|
+
console.error(`[purrx] headers: ${JSON.stringify(safeHeaders)}`);
|
|
102
|
+
console.error(
|
|
103
|
+
`[purrx] body: ${JSON.stringify({ ...payload, input: `<${input.length} items>` })}`
|
|
104
|
+
);
|
|
85
105
|
}
|
|
86
106
|
|
|
87
107
|
const resp = await fetch(url, {
|
|
@@ -92,6 +112,9 @@ export async function streamResponse(
|
|
|
92
112
|
|
|
93
113
|
if (!resp.ok || !resp.body) {
|
|
94
114
|
const text = await resp.text().catch(() => "");
|
|
115
|
+
if (process.env.PURRX_DEBUG) {
|
|
116
|
+
console.error(`[purrx] response ${resp.status}: ${text}`);
|
|
117
|
+
}
|
|
95
118
|
throw new Error(`API request failed (${resp.status}): ${text}`);
|
|
96
119
|
}
|
|
97
120
|
|
package/src/auth/tokens.js
CHANGED
|
@@ -111,7 +111,7 @@ export async function refreshTokens(refreshToken) {
|
|
|
111
111
|
client_id: OAUTH_CLIENT_ID,
|
|
112
112
|
grant_type: "refresh_token",
|
|
113
113
|
refresh_token: refreshToken,
|
|
114
|
-
scope: "openid profile email",
|
|
114
|
+
scope: "openid profile email offline_access",
|
|
115
115
|
}),
|
|
116
116
|
});
|
|
117
117
|
if (!resp.ok) {
|
|
@@ -173,10 +173,22 @@ export function resolveAuthMode(auth) {
|
|
|
173
173
|
}
|
|
174
174
|
if (!auth) return null;
|
|
175
175
|
if (auth.tokens?.access_token) {
|
|
176
|
+
// The chatgpt-account-id header is required by the codex backend. Prefer
|
|
177
|
+
// the stored account_id, but fall back to deriving it from the access or
|
|
178
|
+
// id token claims if it is missing (older logins may not have saved it).
|
|
179
|
+
let accountId = auth.tokens.account_id || null;
|
|
180
|
+
if (!accountId) {
|
|
181
|
+
accountId =
|
|
182
|
+
jwtAuthClaims(auth.tokens.access_token)["chatgpt_account_id"] ||
|
|
183
|
+
(auth.tokens.id_token
|
|
184
|
+
? jwtAuthClaims(auth.tokens.id_token)["chatgpt_account_id"]
|
|
185
|
+
: null) ||
|
|
186
|
+
null;
|
|
187
|
+
}
|
|
176
188
|
return {
|
|
177
189
|
mode: "chatgpt",
|
|
178
190
|
accessToken: auth.tokens.access_token,
|
|
179
|
-
accountId
|
|
191
|
+
accountId,
|
|
180
192
|
};
|
|
181
193
|
}
|
|
182
194
|
if (auth.OPENAI_API_KEY) {
|