@mewbleh/purrx 1.0.10 → 1.0.11

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mewbleh/purrx",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "purrx, a lightweight AI coding agent for your terminal",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/api/client.js CHANGED
@@ -1,11 +1,5 @@
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
-
9
3
  // Builds the request URL and headers for the resolved auth mode.
10
4
  /**
11
5
  * @param {import("../types.js").AuthInfo} authInfo
@@ -13,13 +7,16 @@ function isReasoningModel(model) {
13
7
  */
14
8
  function buildRequest(authInfo) {
15
9
  if (authInfo.mode === "chatgpt") {
10
+ // These headers match the proven-working ChatGPT-codex request shape.
11
+ // The OpenAI-Beta header is required; the backend rejects requests without
12
+ // it (with a misleading "model not supported" error). Do NOT add an
13
+ // `originator` header here: the codex backend does not expect it on this
14
+ // path and its presence can cause the request to be rejected.
16
15
  /** @type {Record<string, string>} */
17
16
  const headers = {
18
17
  "Content-Type": "application/json",
19
18
  Authorization: `Bearer ${authInfo.accessToken}`,
20
- // Codex backend identifies the client via originator + session headers.
21
- originator: "codex_cli_rs",
22
- "User-Agent": "purrx",
19
+ "OpenAI-Beta": "responses=experimental",
23
20
  };
24
21
  if (authInfo.accountId) {
25
22
  headers["chatgpt-account-id"] = authInfo.accountId;
@@ -57,31 +54,24 @@ export async function streamResponse(
57
54
  ) {
58
55
  const { url, headers } = buildRequest(authInfo);
59
56
 
57
+ // Minimal request shape that the ChatGPT-codex backend accepts. The backend
58
+ // requires `instructions` to be present (a string, even if empty) and
59
+ // `store: false`. We deliberately avoid adding reasoning/include/cache fields
60
+ // here: the backend rejects requests it doesn't recognize as a valid Codex
61
+ // session with a misleading "model not supported" error, and the known-good
62
+ // client sends only these fields.
60
63
  /** @type {Record<string, any>} */
61
64
  const payload = {
62
65
  model,
66
+ instructions: instructions || "",
63
67
  input,
64
68
  stream: true,
65
- // The Codex backend requires store:false; responses are not persisted.
66
69
  store: false,
67
- // Stable cache key improves latency/cost across turns (matches Codex).
68
- prompt_cache_key: sessionId || "purrx",
69
70
  };
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
71
 
81
72
  if (tools && tools.length) {
82
73
  payload.tools = tools;
83
74
  payload.tool_choice = "auto";
84
- payload.parallel_tool_calls = true;
85
75
  }
86
76
 
87
77
  const resp = await fetch(url, {
@@ -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: auth.tokens.account_id || null,
191
+ accountId,
180
192
  };
181
193
  }
182
194
  if (auth.OPENAI_API_KEY) {