@mewbleh/purrx 1.0.8 → 1.0.10
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 +1 -0
- package/package.json +1 -1
- package/src/api/client.js +27 -2
- package/src/api/models.js +13 -10
- package/src/config.js +2 -2
- package/src/core/agent.js +23 -15
- package/src/ui/tui.js +1 -0
package/bin/purrx.js
CHANGED
package/package.json
CHANGED
package/src/api/client.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { API_BASE, CHATGPT_BASE } from "../config.js";
|
|
2
2
|
|
|
3
|
+
// Models that use the Responses API reasoning fields. gpt-5 family, o-series,
|
|
4
|
+
// and any *-codex variant are reasoning models.
|
|
5
|
+
function isReasoningModel(model) {
|
|
6
|
+
return /^(gpt-5|o\d|codex)/i.test(model) || /-codex\b/i.test(model);
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
// Builds the request URL and headers for the resolved auth mode.
|
|
4
10
|
/**
|
|
5
11
|
* @param {import("../types.js").AuthInfo} authInfo
|
|
@@ -11,8 +17,9 @@ function buildRequest(authInfo) {
|
|
|
11
17
|
const headers = {
|
|
12
18
|
"Content-Type": "application/json",
|
|
13
19
|
Authorization: `Bearer ${authInfo.accessToken}`,
|
|
14
|
-
|
|
20
|
+
// Codex backend identifies the client via originator + session headers.
|
|
15
21
|
originator: "codex_cli_rs",
|
|
22
|
+
"User-Agent": "purrx",
|
|
16
23
|
};
|
|
17
24
|
if (authInfo.accountId) {
|
|
18
25
|
headers["chatgpt-account-id"] = authInfo.accountId;
|
|
@@ -40,10 +47,14 @@ function buildRequest(authInfo) {
|
|
|
40
47
|
* @param {import("../types.js").HistoryItem[]} req.input
|
|
41
48
|
* @param {import("../types.js").ToolDefinition[]} [req.tools]
|
|
42
49
|
* @param {string} [req.instructions]
|
|
50
|
+
* @param {string} [req.sessionId] stable id used for prompt caching
|
|
43
51
|
* @param {{ onText?: (delta: string) => void, onEvent?: (event: any) => void }} [handlers]
|
|
44
52
|
* @returns {Promise<any>}
|
|
45
53
|
*/
|
|
46
|
-
export async function streamResponse(
|
|
54
|
+
export async function streamResponse(
|
|
55
|
+
{ authInfo, model, input, tools, instructions, sessionId },
|
|
56
|
+
handlers = {}
|
|
57
|
+
) {
|
|
47
58
|
const { url, headers } = buildRequest(authInfo);
|
|
48
59
|
|
|
49
60
|
/** @type {Record<string, any>} */
|
|
@@ -51,12 +62,26 @@ export async function streamResponse({ authInfo, model, input, tools, instructio
|
|
|
51
62
|
model,
|
|
52
63
|
input,
|
|
53
64
|
stream: true,
|
|
65
|
+
// The Codex backend requires store:false; responses are not persisted.
|
|
54
66
|
store: false,
|
|
67
|
+
// Stable cache key improves latency/cost across turns (matches Codex).
|
|
68
|
+
prompt_cache_key: sessionId || "purrx",
|
|
55
69
|
};
|
|
56
70
|
if (instructions) payload.instructions = instructions;
|
|
71
|
+
|
|
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
|
+
if (isReasoningModel(model)) {
|
|
77
|
+
payload.reasoning = { effort: "medium", summary: "auto" };
|
|
78
|
+
payload.include = ["reasoning.encrypted_content"];
|
|
79
|
+
}
|
|
80
|
+
|
|
57
81
|
if (tools && tools.length) {
|
|
58
82
|
payload.tools = tools;
|
|
59
83
|
payload.tool_choice = "auto";
|
|
84
|
+
payload.parallel_tool_calls = true;
|
|
60
85
|
}
|
|
61
86
|
|
|
62
87
|
const resp = await fetch(url, {
|
package/src/api/models.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { API_BASE,
|
|
1
|
+
import { API_BASE, DEFAULT_MODEL } from "../config.js";
|
|
2
2
|
|
|
3
3
|
// Fetches the list of model ids the current account can actually use.
|
|
4
4
|
// - API-key mode: GET https://api.openai.com/v1/models
|
|
5
5
|
// - ChatGPT mode: the codex backend does not expose a public models list the
|
|
6
|
-
// same way, so we
|
|
6
|
+
// same way, so we report a known-good set.
|
|
7
7
|
export async function listModels(authInfo) {
|
|
8
8
|
if (authInfo.mode === "apikey") {
|
|
9
9
|
return listApiKeyModels(authInfo.apiKey);
|
|
@@ -24,18 +24,16 @@ async function listApiKeyModels(apiKey) {
|
|
|
24
24
|
// Keep models usable for chat/agent work (gpt*, o*, codex).
|
|
25
25
|
.filter((/** @type {string} */ id) => /^(gpt|o\d|codex|chatgpt)/i.test(id))
|
|
26
26
|
.sort();
|
|
27
|
-
return ids.length ? ids : fallbackModels();
|
|
27
|
+
return ids.length ? dedupeWithDefault(ids) : fallbackModels();
|
|
28
28
|
} catch {
|
|
29
29
|
return fallbackModels();
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// The ChatGPT/Codex backend exposes models tied to the plan. There is no
|
|
34
|
-
//
|
|
35
|
-
// models. DEFAULT_MODEL is always first.
|
|
33
|
+
// The ChatGPT/Codex backend exposes models tied to the plan. There is no stable
|
|
34
|
+
// public list endpoint, so we report the commonly available Codex models.
|
|
36
35
|
function listChatGptModels() {
|
|
37
|
-
|
|
38
|
-
return dedupeWithDefault(known);
|
|
36
|
+
return dedupeWithDefault(["gpt-5-codex", "gpt-5", "gpt-5-mini", "o4-mini", "o3"]);
|
|
39
37
|
}
|
|
40
38
|
|
|
41
39
|
function fallbackModels() {
|
|
@@ -47,8 +45,13 @@ function dedupeWithDefault(list) {
|
|
|
47
45
|
return [...set];
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
// Resolves the model to use: explicit override >
|
|
51
|
-
//
|
|
48
|
+
// Resolves the model to use: explicit override > a model the account actually
|
|
49
|
+
// has > DEFAULT_MODEL.
|
|
50
|
+
/**
|
|
51
|
+
* @param {import("../types.js").AuthInfo} authInfo
|
|
52
|
+
* @param {string} [preferred]
|
|
53
|
+
* @returns {Promise<string>}
|
|
54
|
+
*/
|
|
52
55
|
export async function resolveModel(authInfo, preferred) {
|
|
53
56
|
if (preferred) return preferred;
|
|
54
57
|
const available = await listModels(authInfo).catch(() => []);
|
package/src/config.js
CHANGED
|
@@ -13,8 +13,8 @@ export const OAUTH_SCOPE =
|
|
|
13
13
|
export const API_BASE = "https://api.openai.com/v1";
|
|
14
14
|
export const CHATGPT_BASE = "https://chatgpt.com/backend-api/codex";
|
|
15
15
|
|
|
16
|
-
// Default model.
|
|
17
|
-
//
|
|
16
|
+
// Default model. gpt-5-codex is the default Codex coding model and works on
|
|
17
|
+
// both API-key and ChatGPT-account auth. Override with PURRX_MODEL.
|
|
18
18
|
export const DEFAULT_MODEL = process.env.PURRX_MODEL || "gpt-5-codex";
|
|
19
19
|
|
|
20
20
|
// Approximate context window (in tokens) used to decide when to auto-compact
|
package/src/core/agent.js
CHANGED
|
@@ -45,6 +45,7 @@ Guidelines:
|
|
|
45
45
|
* @param {import("../tools/registry.js").ToolRegistry} opts.registry
|
|
46
46
|
* @param {{requestApproval: (name: string, label: string) => Promise<string>}} [opts.approval]
|
|
47
47
|
* @param {() => void} [opts.onChange]
|
|
48
|
+
* @param {string} [opts.sessionId] stable id used for prompt caching
|
|
48
49
|
* @returns {Promise<import("../types.js").HistoryItem[]>}
|
|
49
50
|
*/
|
|
50
51
|
export async function runTurn({
|
|
@@ -56,6 +57,7 @@ export async function runTurn({
|
|
|
56
57
|
registry,
|
|
57
58
|
approval,
|
|
58
59
|
onChange,
|
|
60
|
+
sessionId,
|
|
59
61
|
}) {
|
|
60
62
|
history.push({
|
|
61
63
|
type: "message",
|
|
@@ -84,22 +86,28 @@ export async function runTurn({
|
|
|
84
86
|
|
|
85
87
|
spinner.start();
|
|
86
88
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
buffered += delta;
|
|
89
|
+
let finalResponse;
|
|
90
|
+
try {
|
|
91
|
+
finalResponse = await streamResponse(
|
|
92
|
+
{
|
|
93
|
+
authInfo,
|
|
94
|
+
model,
|
|
95
|
+
input: history,
|
|
96
|
+
tools,
|
|
97
|
+
instructions: systemInstructions(cwd, registry),
|
|
98
|
+
sessionId,
|
|
98
99
|
},
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
{
|
|
101
|
+
onText: (delta) => {
|
|
102
|
+
buffered += delta;
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
} finally {
|
|
107
|
+
// Always stop the spinner, even if the request throws, so it never
|
|
108
|
+
// keeps spinning under an error message.
|
|
109
|
+
spinner.stop();
|
|
110
|
+
}
|
|
103
111
|
|
|
104
112
|
if (!finalResponse) {
|
|
105
113
|
throw new Error("No response received from the model.");
|
package/src/ui/tui.js
CHANGED
|
@@ -101,6 +101,7 @@ export async function startTui(authInfo, options = {}) {
|
|
|
101
101
|
registry,
|
|
102
102
|
approval: state.approval,
|
|
103
103
|
onChange: persist,
|
|
104
|
+
sessionId: session.id,
|
|
104
105
|
});
|
|
105
106
|
} catch (err) {
|
|
106
107
|
console.log(box(chalk.red(err.message), { title: "error", borderColor: "red" }));
|