@shanepadgett/tau-agent 0.20.2 → 0.22.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.
@@ -1,40 +1,38 @@
1
1
  import type { SessionEntry } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  const CONTEXT_PRUNE_TOOL = "context_prune";
4
- const AUTOREAD_MESSAGE_TYPE = "tau.autoread";
5
4
 
6
- export interface ContextPruneRefreshedFileV1 {
5
+ export interface ContextPruneRefreshedFileV2 {
7
6
  path: string;
8
7
  rowId: string;
9
8
  servedHash: string;
9
+ autoreadDetails: Record<string, unknown>;
10
10
  }
11
11
 
12
- export interface ContextPruneDeferredFileV1 {
12
+ export interface ContextPruneDeferredFileV2 {
13
13
  path: string;
14
14
  reason: string;
15
15
  relevantWhen: string;
16
16
  }
17
17
 
18
- export interface ContextPruneDetailsV1 {
19
- v: 1;
20
- status: "applied" | "skipped";
18
+ export interface ContextPruneDetailsV2 {
19
+ v: 2;
21
20
  anchorToolCallId: string;
22
- newlyPrunedToolCallIds: string[];
23
- newlyPrunedAutoreadRowIds: string[];
21
+ prunedToolCallIds: string[];
22
+ prunedAutoreadRowIds: string[];
24
23
  retainedToolCallIds: string[];
25
24
  retainedAutoreadRowIds: string[];
26
- refreshedFiles: ContextPruneRefreshedFileV1[];
27
- deferredFiles: ContextPruneDeferredFileV1[];
28
- tokensBefore: number;
29
- tokensAfter: number;
30
- tokensReclaimed: number;
25
+ refreshedFiles: ContextPruneRefreshedFileV2[];
26
+ deferredFiles: ContextPruneDeferredFileV2[];
27
+ warnings: string[];
31
28
  }
32
29
 
33
30
  export interface ActiveContextPruningState {
34
31
  latestAnchorToolCallId: string | undefined;
32
+ retainedToolCallIds: ReadonlySet<string>;
35
33
  prunedToolCallIds: ReadonlySet<string>;
36
34
  prunedAutoreadRowIds: ReadonlySet<string>;
37
- deferredFiles: readonly ContextPruneDeferredFileV1[];
35
+ deferredFiles: readonly ContextPruneDeferredFileV2[];
38
36
  }
39
37
 
40
38
  const RUNTIME_ENABLED_KEY = Symbol.for("@earendil-works/tau-agent/context-pruning-enabled");
@@ -48,67 +46,35 @@ function isContextPruningEnabled(): boolean {
48
46
  return runtimeState[RUNTIME_ENABLED_KEY] === true;
49
47
  }
50
48
 
51
- export function parseContextPruneDetailsV1(value: unknown): ContextPruneDetailsV1 | undefined {
52
- if (
53
- !hasExactKeys(value, [
54
- "v",
55
- "status",
56
- "anchorToolCallId",
57
- "newlyPrunedToolCallIds",
58
- "newlyPrunedAutoreadRowIds",
59
- "retainedToolCallIds",
60
- "retainedAutoreadRowIds",
61
- "refreshedFiles",
62
- "deferredFiles",
63
- "tokensBefore",
64
- "tokensAfter",
65
- "tokensReclaimed",
66
- ])
67
- )
68
- return undefined;
69
- if (value.v !== 1 || (value.status !== "applied" && value.status !== "skipped")) return undefined;
70
- if (!isNonEmptyString(value.anchorToolCallId)) return undefined;
71
-
72
- const newlyPrunedToolCallIds = parseUniqueStrings(value.newlyPrunedToolCallIds);
73
- const newlyPrunedAutoreadRowIds = parseUniqueStrings(value.newlyPrunedAutoreadRowIds);
74
- const retainedToolCallIds = parseUniqueStrings(value.retainedToolCallIds);
49
+ export function parseContextPruneDetailsV2(value: unknown): ContextPruneDetailsV2 | undefined {
50
+ if (!isRecord(value) || value.v !== 2 || !isNonEmptyString(value.anchorToolCallId)) return undefined;
51
+ const prunedToolCallIds = parseUniqueStrings(value.prunedToolCallIds);
52
+ const prunedAutoreadRowIds = parseUniqueStrings(value.prunedAutoreadRowIds);
53
+ const retainedToolCallIds = parseUniqueStrings(value.retainedToolCallIds, true);
75
54
  const retainedAutoreadRowIds = parseUniqueStrings(value.retainedAutoreadRowIds);
76
55
  const refreshedFiles = parseRefreshedFiles(value.refreshedFiles);
77
56
  const deferredFiles = parseDeferredFiles(value.deferredFiles);
57
+ const warnings = parseStrings(value.warnings, true);
78
58
  if (
79
- !newlyPrunedToolCallIds ||
80
- !newlyPrunedAutoreadRowIds ||
59
+ !prunedToolCallIds ||
60
+ !prunedAutoreadRowIds ||
81
61
  !retainedToolCallIds ||
82
62
  !retainedAutoreadRowIds ||
83
63
  !refreshedFiles ||
84
- !deferredFiles
85
- )
86
- return undefined;
87
- if (
88
- !isNonNegativeFinite(value.tokensBefore) ||
89
- !isNonNegativeFinite(value.tokensAfter) ||
90
- !isFiniteNumber(value.tokensReclaimed) ||
91
- value.tokensBefore - value.tokensAfter !== value.tokensReclaimed
64
+ !deferredFiles ||
65
+ !warnings
92
66
  )
93
67
  return undefined;
94
- if (hasOverlap(newlyPrunedToolCallIds, retainedToolCallIds)) return undefined;
95
- if (hasOverlap(newlyPrunedAutoreadRowIds, retainedAutoreadRowIds)) return undefined;
96
- if (value.status === "skipped" && (newlyPrunedToolCallIds.length > 0 || newlyPrunedAutoreadRowIds.length > 0))
97
- return undefined;
98
-
99
68
  return {
100
- v: 1,
101
- status: value.status,
69
+ v: 2,
102
70
  anchorToolCallId: value.anchorToolCallId,
103
- newlyPrunedToolCallIds,
104
- newlyPrunedAutoreadRowIds,
71
+ prunedToolCallIds,
72
+ prunedAutoreadRowIds,
105
73
  retainedToolCallIds,
106
74
  retainedAutoreadRowIds,
107
75
  refreshedFiles,
108
76
  deferredFiles,
109
- tokensBefore: value.tokensBefore,
110
- tokensAfter: value.tokensAfter,
111
- tokensReclaimed: value.tokensReclaimed,
77
+ warnings,
112
78
  };
113
79
  }
114
80
 
@@ -118,233 +84,71 @@ export function replayContextPruningState(
118
84
  ): ActiveContextPruningState {
119
85
  const prunedToolCallIds = new Set<string>();
120
86
  const prunedAutoreadRowIds = new Set<string>();
121
- const state: ActiveContextPruningState = {
122
- latestAnchorToolCallId: undefined,
87
+ let latestAnchorToolCallId: string | undefined;
88
+ let retainedToolCallIds = new Set<string>();
89
+ let deferredFiles: readonly ContextPruneDeferredFileV2[] = [];
90
+ if (enabled) {
91
+ for (const entry of branch) {
92
+ if (entry.type !== "message" || entry.message.role !== "toolResult") continue;
93
+ const result = entry.message;
94
+ if (result.toolName !== CONTEXT_PRUNE_TOOL) continue;
95
+ const details = parseContextPruneDetailsV2(result.details);
96
+ if (!details || details.anchorToolCallId !== result.toolCallId) continue;
97
+ for (const id of details.prunedToolCallIds) prunedToolCallIds.add(id);
98
+ for (const id of details.prunedAutoreadRowIds) prunedAutoreadRowIds.add(id);
99
+ latestAnchorToolCallId = details.anchorToolCallId;
100
+ retainedToolCallIds = new Set(details.retainedToolCallIds);
101
+ deferredFiles = details.deferredFiles;
102
+ }
103
+ for (const id of retainedToolCallIds) prunedToolCallIds.delete(id);
104
+ }
105
+ return {
106
+ latestAnchorToolCallId,
107
+ retainedToolCallIds,
123
108
  prunedToolCallIds,
124
109
  prunedAutoreadRowIds,
125
- deferredFiles: [],
110
+ deferredFiles,
126
111
  };
127
- if (!enabled) return state;
128
-
129
- const toolCalls = new Map<string, { count: number; name: string; index: number }>();
130
- const toolResults = new Map<string, { count: number; name: string; index: number }>();
131
- const autoreads = new Map<
132
- string,
133
- { count: number; path: string; servedHash: string; source: string; batchId: string; index: number }
134
- >();
135
- for (let index = 0; index < branch.length; index += 1) {
136
- const entry = branch[index];
137
- if (entry.type === "message") {
138
- const message = entry.message;
139
- if (message.role === "assistant") {
140
- for (const block of message.content) {
141
- if (block.type !== "toolCall") continue;
142
- const current = toolCalls.get(block.id);
143
- toolCalls.set(block.id, { count: (current?.count ?? 0) + 1, name: block.name, index });
144
- }
145
- } else if (message.role === "toolResult") {
146
- const current = toolResults.get(message.toolCallId);
147
- toolResults.set(message.toolCallId, {
148
- count: (current?.count ?? 0) + 1,
149
- name: message.toolName,
150
- index,
151
- });
152
- }
153
- continue;
154
- }
155
- if (entry.type !== "custom_message" || entry.customType !== AUTOREAD_MESSAGE_TYPE) continue;
156
- const autoread = parseAutoreadReference(entry.details);
157
- if (!autoread) continue;
158
- const current = autoreads.get(autoread.rowId);
159
- autoreads.set(autoread.rowId, {
160
- count: (current?.count ?? 0) + 1,
161
- path: autoread.path,
162
- servedHash: autoread.servedHash,
163
- source: autoread.source,
164
- batchId: autoread.batchId,
165
- index,
166
- });
167
- }
168
-
169
- for (let resultIndex = 0; resultIndex < branch.length; resultIndex += 1) {
170
- const entry = branch[resultIndex];
171
- if (entry.type !== "message" || entry.message.role !== "toolResult") continue;
172
- const result = entry.message;
173
- if (result.toolName !== CONTEXT_PRUNE_TOOL) continue;
174
- const details = parseContextPruneDetailsV1(result.details);
175
- if (!details || details.status !== "applied" || details.anchorToolCallId !== result.toolCallId) continue;
176
- if (!isCompleteExchange(details.anchorToolCallId, CONTEXT_PRUNE_TOOL, toolCalls, toolResults)) continue;
177
- const anchorCallIndex = toolCalls.get(details.anchorToolCallId)?.index;
178
- if (anchorCallIndex === undefined || anchorCallIndex >= resultIndex) continue;
179
- if (details.newlyPrunedToolCallIds.includes(details.anchorToolCallId)) continue;
180
- if (details.retainedToolCallIds.includes(details.anchorToolCallId)) continue;
181
- if (
182
- !allCompleteExchanges(details.newlyPrunedToolCallIds, toolCalls, toolResults) ||
183
- !allCompleteExchanges(details.retainedToolCallIds, toolCalls, toolResults) ||
184
- !allAutoreads(details.newlyPrunedAutoreadRowIds, autoreads) ||
185
- !allAutoreads(details.retainedAutoreadRowIds, autoreads)
186
- )
187
- continue;
188
- if (
189
- details.newlyPrunedToolCallIds.some((id) => !exchangePrecedes(id, anchorCallIndex, toolCalls, toolResults)) ||
190
- details.retainedToolCallIds.some((id) => !exchangePrecedes(id, anchorCallIndex, toolCalls, toolResults)) ||
191
- details.newlyPrunedAutoreadRowIds.some((id) => {
192
- const row = autoreads.get(id);
193
- return row === undefined || row.index >= anchorCallIndex;
194
- })
195
- )
196
- continue;
197
- if (
198
- details.newlyPrunedToolCallIds.some((id) => prunedToolCallIds.has(id)) ||
199
- details.retainedToolCallIds.some((id) => prunedToolCallIds.has(id)) ||
200
- details.newlyPrunedAutoreadRowIds.some((id) => prunedAutoreadRowIds.has(id)) ||
201
- details.retainedAutoreadRowIds.some((id) => prunedAutoreadRowIds.has(id))
202
- )
203
- continue;
204
- if (
205
- details.refreshedFiles.some((file) => {
206
- const row = autoreads.get(file.rowId);
207
- return (
208
- row?.count !== 1 ||
209
- row.path !== file.path ||
210
- row.servedHash !== file.servedHash ||
211
- row.source !== "context-pruning" ||
212
- row.batchId !== details.anchorToolCallId ||
213
- !details.retainedAutoreadRowIds.includes(file.rowId)
214
- );
215
- })
216
- )
217
- continue;
218
- const refreshedRowIds = new Set(details.refreshedFiles.map((file) => file.rowId));
219
- if (
220
- details.retainedAutoreadRowIds.some((id) => {
221
- const row = autoreads.get(id);
222
- return row === undefined || (row.index >= anchorCallIndex && !refreshedRowIds.has(id));
223
- })
224
- )
225
- continue;
226
-
227
- for (const id of details.newlyPrunedToolCallIds) prunedToolCallIds.add(id);
228
- for (const id of details.newlyPrunedAutoreadRowIds) prunedAutoreadRowIds.add(id);
229
- state.latestAnchorToolCallId = details.anchorToolCallId;
230
- state.deferredFiles = details.deferredFiles;
231
- }
232
- return state;
233
112
  }
234
113
 
235
- function parseRefreshedFiles(value: unknown): ContextPruneRefreshedFileV1[] | undefined {
114
+ function parseRefreshedFiles(value: unknown): ContextPruneRefreshedFileV2[] | undefined {
236
115
  if (!Array.isArray(value)) return undefined;
237
- const files: ContextPruneRefreshedFileV1[] = [];
238
- const rowIds = new Set<string>();
116
+ const files: ContextPruneRefreshedFileV2[] = [];
239
117
  for (const item of value) {
240
- if (!hasExactKeys(item, ["path", "rowId", "servedHash"])) return undefined;
241
- if (!isNonEmptyString(item.path) || !isNonEmptyString(item.rowId) || !isNonEmptyString(item.servedHash))
242
- return undefined;
243
- if (rowIds.has(item.rowId)) return undefined;
244
- rowIds.add(item.rowId);
245
- files.push({ path: item.path, rowId: item.rowId, servedHash: item.servedHash });
118
+ if (!isRecord(item) || !isNonEmptyString(item.path) || !isNonEmptyString(item.rowId)) return undefined;
119
+ if (!isNonEmptyString(item.servedHash) || !isRecord(item.autoreadDetails)) return undefined;
120
+ files.push({
121
+ path: item.path,
122
+ rowId: item.rowId,
123
+ servedHash: item.servedHash,
124
+ autoreadDetails: { ...item.autoreadDetails },
125
+ });
246
126
  }
247
127
  return files;
248
128
  }
249
129
 
250
- function parseDeferredFiles(value: unknown): ContextPruneDeferredFileV1[] | undefined {
130
+ function parseDeferredFiles(value: unknown): ContextPruneDeferredFileV2[] | undefined {
251
131
  if (!Array.isArray(value)) return undefined;
252
- const files: ContextPruneDeferredFileV1[] = [];
132
+ const files: ContextPruneDeferredFileV2[] = [];
253
133
  for (const item of value) {
254
- if (!hasExactKeys(item, ["path", "reason", "relevantWhen"])) return undefined;
255
- if (!isNonEmptyString(item.path) || !isNonEmptyString(item.reason) || !isNonEmptyString(item.relevantWhen))
256
- return undefined;
134
+ if (!isRecord(item) || typeof item.path !== "string" || typeof item.reason !== "string") return undefined;
135
+ if (typeof item.relevantWhen !== "string") return undefined;
257
136
  files.push({ path: item.path, reason: item.reason, relevantWhen: item.relevantWhen });
258
137
  }
259
138
  return files;
260
139
  }
261
140
 
262
- function parseUniqueStrings(value: unknown): string[] | undefined {
263
- if (!Array.isArray(value)) return undefined;
264
- const strings: string[] = [];
265
- const seen = new Set<string>();
266
- for (const item of value) {
267
- if (!isNonEmptyString(item) || seen.has(item)) return undefined;
268
- seen.add(item);
269
- strings.push(item);
270
- }
141
+ function parseUniqueStrings(value: unknown, allowEmpty = false): string[] | undefined {
142
+ const strings = parseStrings(value, allowEmpty);
143
+ if (!strings || new Set(strings).size !== strings.length) return undefined;
271
144
  return strings;
272
145
  }
273
146
 
274
- function parseAutoreadReference(
275
- value: unknown,
276
- ): { rowId: string; path: string; servedHash: string; source: string; batchId: string } | undefined {
277
- if (!isRecord(value) || value.status !== "read") return undefined;
278
- if (
279
- !isNonEmptyString(value.rowId) ||
280
- !isNonEmptyString(value.path) ||
281
- !isNonEmptyString(value.cwd) ||
282
- !isNonEmptyString(value.source) ||
283
- !isNonEmptyString(value.batchId)
284
- )
147
+ function parseStrings(value: unknown, allowEmpty = false): string[] | undefined {
148
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string" && (allowEmpty || item.length > 0))) {
285
149
  return undefined;
286
- if (!isRecord(value.readCache) || !isNonEmptyString(value.readCache.servedHash)) return undefined;
287
- return {
288
- rowId: value.rowId,
289
- path: value.path,
290
- servedHash: value.readCache.servedHash,
291
- source: value.source,
292
- batchId: value.batchId,
293
- };
294
- }
295
-
296
- function allCompleteExchanges(
297
- ids: readonly string[],
298
- calls: ReadonlyMap<string, { count: number; name: string; index: number }>,
299
- results: ReadonlyMap<string, { count: number; name: string; index: number }>,
300
- ): boolean {
301
- return ids.every((id) => {
302
- const call = calls.get(id);
303
- return call !== undefined && isCompleteExchange(id, call.name, calls, results);
304
- });
305
- }
306
-
307
- function isCompleteExchange(
308
- id: string,
309
- name: string,
310
- calls: ReadonlyMap<string, { count: number; name: string; index: number }>,
311
- results: ReadonlyMap<string, { count: number; name: string; index: number }>,
312
- ): boolean {
313
- const call = calls.get(id);
314
- const result = results.get(id);
315
- return (
316
- call?.count === 1 &&
317
- result?.count === 1 &&
318
- call.name === name &&
319
- result.name === name &&
320
- call.index < result.index
321
- );
322
- }
323
-
324
- function exchangePrecedes(
325
- id: string,
326
- anchorCallIndex: number,
327
- calls: ReadonlyMap<string, { index: number }>,
328
- results: ReadonlyMap<string, { index: number }>,
329
- ): boolean {
330
- const call = calls.get(id);
331
- const result = results.get(id);
332
- return call !== undefined && result !== undefined && call.index < result.index && result.index < anchorCallIndex;
333
- }
334
-
335
- function allAutoreads(ids: readonly string[], autoreads: ReadonlyMap<string, { count: number }>): boolean {
336
- return ids.every((id) => autoreads.get(id)?.count === 1);
337
- }
338
-
339
- function hasOverlap(left: readonly string[], right: readonly string[]): boolean {
340
- const rightSet = new Set(right);
341
- return left.some((item) => rightSet.has(item));
342
- }
343
-
344
- function hasExactKeys(value: unknown, keys: readonly string[]): value is Record<string, unknown> {
345
- if (!isRecord(value)) return false;
346
- const actual = Object.keys(value);
347
- return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
150
+ }
151
+ return [...value];
348
152
  }
349
153
 
350
154
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -354,11 +158,3 @@ function isRecord(value: unknown): value is Record<string, unknown> {
354
158
  function isNonEmptyString(value: unknown): value is string {
355
159
  return typeof value === "string" && value.length > 0;
356
160
  }
357
-
358
- function isNonNegativeFinite(value: unknown): value is number {
359
- return typeof value === "number" && Number.isFinite(value) && value >= 0;
360
- }
361
-
362
- function isFiniteNumber(value: unknown): value is number {
363
- return typeof value === "number" && Number.isFinite(value);
364
- }
@@ -24,7 +24,7 @@ interface ModelFallbackOptions {
24
24
 
25
25
  export async function resolveCandidates(
26
26
  ctx: Pick<ExtensionContext, "modelRegistry" | "model" | "cwd" | "isProjectTrusted">,
27
- preferredModels: ReadonlyArray<{ provider: string; model: string; reasoning: ThinkingLevel }>,
27
+ preferredModels: ReadonlyArray<{ provider: string; model: string; reasoning: ThinkingLevel | undefined }>,
28
28
  includeParentModel: boolean,
29
29
  ): Promise<ModelCandidate[]> {
30
30
  const settings = await loadTauExtensionSettings(ctx, modelFallbackSettings);
@@ -24,7 +24,6 @@ export interface EditImage {
24
24
 
25
25
  export interface GeneratedImage {
26
26
  bytes: Buffer;
27
- base64: string;
28
27
  mimeType: EditImage["mimeType"];
29
28
  }
30
29
 
@@ -58,7 +57,7 @@ export function resolveCodexAuth(token: string): CodexAuth {
58
57
  const authClaim = payload["https://api.openai.com/auth"];
59
58
  if (!isRecord(authClaim)) throw invalidCredential();
60
59
  const accountId = authClaim.chatgpt_account_id;
61
- if (typeof accountId !== "string" || !accountId.trim()) throw invalidCredential();
60
+ if (typeof accountId !== "string" || !/^[\x21-\x7e]+$/.test(accountId.trim())) throw invalidCredential();
62
61
  return { token, accountId: accountId.trim() };
63
62
  }
64
63
 
@@ -87,7 +86,7 @@ async function boundedError(response: HttpResponse): Promise<string> {
87
86
  .trim();
88
87
  }
89
88
 
90
- function serverErrorMessage(body: string, token: string): string {
89
+ function serverErrorMessage(body: string, secrets: readonly string[]): string {
91
90
  let message = body;
92
91
  try {
93
92
  const value: unknown = JSON.parse(body);
@@ -98,7 +97,10 @@ function serverErrorMessage(body: string, token: string): string {
98
97
  } catch {
99
98
  // Plain-text error body.
100
99
  }
101
- return message.replaceAll(token, "[redacted]").slice(0, MAX_ERROR_MESSAGE_LENGTH).trim();
100
+ for (const secret of secrets) {
101
+ if (secret) message = message.replaceAll(secret, "[redacted]");
102
+ }
103
+ return message.slice(0, MAX_ERROR_MESSAGE_LENGTH).trim();
102
104
  }
103
105
 
104
106
  export function detectImageMimeType(bytes: Buffer): GeneratedImage["mimeType"] | undefined {
@@ -137,7 +139,7 @@ function decodeImageResponse(value: unknown, service: "OpenAI Codex" | "xAI", pn
137
139
  if (!mimeType || (pngOnly && mimeType !== "image/png")) {
138
140
  throw new Error(`${service} returned unsupported image data`);
139
141
  }
140
- return { bytes, base64: bytes.toString("base64"), mimeType };
142
+ return { bytes, mimeType };
141
143
  }
142
144
 
143
145
  async function requestOpenAIImage(
@@ -147,20 +149,28 @@ async function requestOpenAIImage(
147
149
  signal?: AbortSignal,
148
150
  ): Promise<GeneratedImage> {
149
151
  const route = operation === "generation" ? "generations" : "edits";
150
- const response = (await fetch(`${OPENAI_IMAGE_API_BASE_URL}/${route}`, {
151
- method: "POST",
152
- headers: {
153
- Accept: "application/json",
154
- Authorization: `Bearer ${auth.token}`,
155
- "chatgpt-account-id": auth.accountId,
156
- "Content-Type": "application/json",
157
- originator: "pi",
158
- },
159
- body: JSON.stringify(body),
160
- signal,
161
- })) as HttpResponse;
152
+ let response: HttpResponse;
153
+ try {
154
+ response = (await fetch(`${OPENAI_IMAGE_API_BASE_URL}/${route}`, {
155
+ method: "POST",
156
+ headers: {
157
+ Accept: "application/json",
158
+ Authorization: `Bearer ${auth.token}`,
159
+ "chatgpt-account-id": auth.accountId,
160
+ "Content-Type": "application/json",
161
+ originator: "pi",
162
+ },
163
+ body: JSON.stringify(body),
164
+ signal,
165
+ })) as HttpResponse;
166
+ } catch (error) {
167
+ if (signal?.aborted) throw signal.reason;
168
+ let message = error instanceof Error ? error.message : "OpenAI Codex image request failed";
169
+ for (const secret of [auth.token, auth.accountId]) message = message.replaceAll(secret, "[redacted]");
170
+ throw new Error(message.slice(0, MAX_ERROR_MESSAGE_LENGTH).trim() || "OpenAI Codex image request failed");
171
+ }
162
172
  if (!response.ok) {
163
- const message = serverErrorMessage(await boundedError(response), auth.token);
173
+ const message = serverErrorMessage(await boundedError(response), [auth.token, auth.accountId]);
164
174
  throw new Error(
165
175
  `OpenAI Codex image ${operation} failed with status ${response.status}${message ? `: ${message}` : ""}`,
166
176
  );
@@ -232,7 +242,7 @@ async function requestXaiImage(
232
242
  }
233
243
  return decodeImageResponse(value, "xAI", false);
234
244
  }
235
- const message = serverErrorMessage(await boundedError(response), token);
245
+ const message = serverErrorMessage(await boundedError(response), [token]);
236
246
  if (!retryable(response.status) || attempt === MAX_ATTEMPTS) {
237
247
  throw new Error(
238
248
  `xAI image ${operation} failed with status ${response.status}${message ? `: ${message}` : ""}`,
@@ -243,7 +253,7 @@ async function requestXaiImage(
243
253
  throw new Error(`xAI image ${operation} failed`);
244
254
  }
245
255
 
246
- export function generateImage(
256
+ export function requestGeneratedImage(
247
257
  provider: ImageProvider,
248
258
  prompt: string,
249
259
  token: string,