@mewbleh/purrx 1.0.14 → 1.0.16
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 +1 -1
- package/src/api/client.js +41 -23
- package/src/auth/login.js +3 -0
- package/src/auth/tokens.js +2 -1
- package/src/config.js +22 -0
package/package.json
CHANGED
package/src/api/client.js
CHANGED
|
@@ -9,10 +9,13 @@ function isReasoningModel(model) {
|
|
|
9
9
|
|
|
10
10
|
// Builds the request URL and headers for the resolved auth mode.
|
|
11
11
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
12
|
+
// IMPORTANT: For the ChatGPT-codex backend we deliberately send the MINIMAL
|
|
13
|
+
// header set that the proven-working community proxy uses
|
|
14
|
+
// (Authorization + ChatGPT-Account-ID + OpenAI-Beta). We do NOT send
|
|
15
|
+
// `originator`/`version`/`session-id`: claiming to be the official Codex CLI
|
|
16
|
+
// makes the backend apply stricter validation that rejects the request with a
|
|
17
|
+
// misleading "model not supported" error. Set PURRX_CODEX_HEADERS=1 to opt into
|
|
18
|
+
// the full Codex-style header set for experimentation.
|
|
16
19
|
/**
|
|
17
20
|
* @param {import("../types.js").AuthInfo} authInfo
|
|
18
21
|
* @param {string} [sessionId]
|
|
@@ -25,16 +28,17 @@ function buildRequest(authInfo, sessionId) {
|
|
|
25
28
|
"Content-Type": "application/json",
|
|
26
29
|
Authorization: `Bearer ${authInfo.accessToken}`,
|
|
27
30
|
"OpenAI-Beta": "responses=experimental",
|
|
28
|
-
originator: "codex_cli_rs",
|
|
29
|
-
// The codex backend requires this version header to accept the request.
|
|
30
|
-
version: CODEX_CLI_VERSION,
|
|
31
|
-
"User-Agent": `codex_cli_rs/${CODEX_CLI_VERSION} (purrx)`,
|
|
32
31
|
};
|
|
33
32
|
if (authInfo.accountId) {
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
headers["chatgpt-account-id"] = authInfo.accountId;
|
|
34
|
+
}
|
|
35
|
+
// Opt-in: full Codex-CLI-style headers (originator/version/session-id).
|
|
36
|
+
if (process.env.PURRX_CODEX_HEADERS) {
|
|
37
|
+
headers.originator = "codex_cli_rs";
|
|
38
|
+
headers.version = CODEX_CLI_VERSION;
|
|
39
|
+
headers["User-Agent"] = `codex_cli_rs/${CODEX_CLI_VERSION} (purrx)`;
|
|
40
|
+
if (sessionId) headers["session-id"] = sessionId;
|
|
36
41
|
}
|
|
37
|
-
if (sessionId) headers["session-id"] = sessionId;
|
|
38
42
|
return { url: `${CHATGPT_BASE}/responses`, headers };
|
|
39
43
|
}
|
|
40
44
|
// API-key mode.
|
|
@@ -67,31 +71,45 @@ export async function streamResponse(
|
|
|
67
71
|
handlers = {}
|
|
68
72
|
) {
|
|
69
73
|
const { url, headers } = buildRequest(authInfo, sessionId);
|
|
74
|
+
const isChatGpt = authInfo.mode === "chatgpt";
|
|
75
|
+
|
|
76
|
+
// Minimal body matching the proven-working community proxy: instructions,
|
|
77
|
+
// store, stream, plus input/tools. We avoid reasoning/include/prompt_cache_key
|
|
78
|
+
// and server-side tools (web_search) on the ChatGPT path, since those can
|
|
79
|
+
// trigger the backend's misleading "model not supported" rejection.
|
|
80
|
+
// Set PURRX_FULL_BODY=1 to send the richer Codex-style body.
|
|
81
|
+
const fullBody = !isChatGpt || process.env.PURRX_FULL_BODY;
|
|
70
82
|
|
|
71
|
-
// Request body matching the official Codex ResponsesApiRequest. These fields
|
|
72
|
-
// are always present in the real client; sending a stripped-down body is what
|
|
73
|
-
// triggers the backend's misleading "model is not supported" rejection.
|
|
74
83
|
/** @type {Record<string, any>} */
|
|
75
84
|
const payload = {
|
|
76
85
|
model,
|
|
77
86
|
instructions: instructions || "",
|
|
78
87
|
input,
|
|
79
|
-
tool_choice: "auto",
|
|
80
|
-
parallel_tool_calls: false,
|
|
81
88
|
store: false,
|
|
82
89
|
stream: true,
|
|
83
|
-
include: [],
|
|
84
90
|
};
|
|
85
91
|
|
|
86
|
-
if (
|
|
87
|
-
payload.
|
|
88
|
-
payload.
|
|
92
|
+
if (fullBody) {
|
|
93
|
+
payload.tool_choice = "auto";
|
|
94
|
+
payload.parallel_tool_calls = false;
|
|
95
|
+
payload.include = [];
|
|
96
|
+
if (isReasoningModel(model)) {
|
|
97
|
+
payload.reasoning = { effort: "medium", summary: "auto" };
|
|
98
|
+
payload.include = ["reasoning.encrypted_content"];
|
|
99
|
+
}
|
|
100
|
+
if (sessionId) payload.prompt_cache_key = sessionId;
|
|
89
101
|
}
|
|
90
102
|
|
|
91
|
-
if (sessionId) payload.prompt_cache_key = sessionId;
|
|
92
|
-
|
|
93
103
|
if (tools && tools.length) {
|
|
94
|
-
|
|
104
|
+
// On the ChatGPT path, only send function tools (drop server-side tools
|
|
105
|
+
// like web_search which the codex backend may reject).
|
|
106
|
+
const filtered = isChatGpt
|
|
107
|
+
? tools.filter((t) => t.type === "function")
|
|
108
|
+
: tools;
|
|
109
|
+
if (filtered.length) {
|
|
110
|
+
payload.tools = filtered;
|
|
111
|
+
if (!payload.tool_choice) payload.tool_choice = "auto";
|
|
112
|
+
}
|
|
95
113
|
}
|
|
96
114
|
|
|
97
115
|
// Debug: dump the exact outgoing request when PURRX_DEBUG is set. Auth tokens
|
package/src/auth/login.js
CHANGED
|
@@ -25,6 +25,9 @@ function buildAuthorizeUrl(redirectUri, pkce, state) {
|
|
|
25
25
|
id_token_add_organizations: "true",
|
|
26
26
|
codex_cli_simplified_flow: "true",
|
|
27
27
|
state,
|
|
28
|
+
// The official Codex authorize request includes the originator; the issued
|
|
29
|
+
// token's Codex entitlement can depend on it.
|
|
30
|
+
originator: "codex_cli_rs",
|
|
28
31
|
});
|
|
29
32
|
return `${OAUTH_ISSUER}/oauth/authorize?${params.toString()}`;
|
|
30
33
|
}
|
package/src/auth/tokens.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import {
|
|
3
3
|
authFilePath,
|
|
4
|
+
resolveAuthFileForRead,
|
|
4
5
|
purrxHome,
|
|
5
6
|
OAUTH_CLIENT_ID,
|
|
6
7
|
OAUTH_ISSUER,
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
|
|
16
17
|
/** @returns {AuthDotJson|null} */
|
|
17
18
|
export function readAuth() {
|
|
18
|
-
const file =
|
|
19
|
+
const file = resolveAuthFileForRead();
|
|
19
20
|
try {
|
|
20
21
|
const raw = fs.readFileSync(file, "utf8");
|
|
21
22
|
return JSON.parse(raw);
|
package/src/config.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import fs from "node:fs";
|
|
2
4
|
import { dataHome } from "./platform.js";
|
|
3
5
|
|
|
4
6
|
// OAuth constants taken from the official OpenAI Codex CLI so we can reuse the
|
|
@@ -43,6 +45,26 @@ export function authFilePath() {
|
|
|
43
45
|
return path.join(purrxHome(), "auth.json");
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
// Resolves the auth.json to READ from. Prefers purrx's own file, then falls
|
|
49
|
+
// back to the official Codex CLI locations (CODEX_HOME, ~/.codex). This lets
|
|
50
|
+
// purrx reuse a known-good token minted by `codex login`.
|
|
51
|
+
export function resolveAuthFileForRead() {
|
|
52
|
+
const candidates = [
|
|
53
|
+
process.env.PURRX_HOME && path.join(process.env.PURRX_HOME, "auth.json"),
|
|
54
|
+
path.join(purrxHome(), "auth.json"),
|
|
55
|
+
process.env.CODEX_HOME && path.join(process.env.CODEX_HOME, "auth.json"),
|
|
56
|
+
path.join(os.homedir(), ".codex", "auth.json"),
|
|
57
|
+
].filter(Boolean);
|
|
58
|
+
for (const candidate of candidates) {
|
|
59
|
+
try {
|
|
60
|
+
if (fs.existsSync(/** @type {string} */ (candidate))) return candidate;
|
|
61
|
+
} catch {
|
|
62
|
+
// ignore
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return authFilePath();
|
|
66
|
+
}
|
|
67
|
+
|
|
46
68
|
export function sessionsDir() {
|
|
47
69
|
return path.join(purrxHome(), "sessions");
|
|
48
70
|
}
|