pi-openai-codex-compat 0.0.1 → 0.0.3

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.
@@ -11,6 +11,8 @@ export type ReasoningMode = "standard" | "pro";
11
11
  export type ImageDetail = "auto" | "low" | "high" | "original";
12
12
  export type CodexToolBackground = "subtle" | "status" | "none";
13
13
 
14
+ export const ENV_PREFIX = "PI_OPENAI_CODEX_COMPAT_";
15
+
14
16
  export interface CodexCompatConfig {
15
17
  /** Send OpenAI Codex requests through the priority service tier. */
16
18
  fastMode: boolean;
@@ -36,6 +38,20 @@ export interface CodexCompatConfig {
36
38
  reasoningMode: ReasoningMode;
37
39
  }
38
40
 
41
+ export const CONFIG_ENVIRONMENT_VARIABLES = {
42
+ fastMode: `${ENV_PREFIX}FAST_MODE`,
43
+ applyPatch: `${ENV_PREFIX}APPLY_PATCH`,
44
+ toolBackground: `${ENV_PREFIX}TOOL_BACKGROUND`,
45
+ imageGeneration: `${ENV_PREFIX}IMAGE_GENERATION`,
46
+ imageDetail: `${ENV_PREFIX}IMAGE_DETAIL`,
47
+ webRun: `${ENV_PREFIX}WEB_RUN`,
48
+ autoCompactAtPercent: `${ENV_PREFIX}AUTO_COMPACT_AT_PERCENT`,
49
+ webSearch: `${ENV_PREFIX}WEB_SEARCH_MODE`,
50
+ textVerbosity: `${ENV_PREFIX}TEXT_VERBOSITY`,
51
+ reasoningSummary: `${ENV_PREFIX}REASONING_SUMMARY`,
52
+ reasoningMode: `${ENV_PREFIX}REASONING_MODE`,
53
+ } as const satisfies Record<keyof CodexCompatConfig, string>;
54
+
39
55
  export type ConfigLayer = {
40
56
  fastMode?: boolean;
41
57
  applyPatch?: boolean;
@@ -57,8 +73,8 @@ export const DEFAULT_CONFIG: CodexCompatConfig = {
57
73
  toolBackground: "subtle",
58
74
  imageGeneration: true,
59
75
  imageDetail: "auto",
60
- webRun: true,
61
- webSearch: "cached",
76
+ webRun: false,
77
+ webSearch: "disabled",
62
78
  textVerbosity: "low",
63
79
  reasoningSummary: "auto",
64
80
  reasoningMode: "standard",
@@ -71,10 +87,136 @@ const REASONING_MODES = new Set<ReasoningMode>(["standard", "pro"]);
71
87
  const IMAGE_DETAILS = new Set<ImageDetail>(["auto", "low", "high", "original"]);
72
88
  const CODEX_TOOL_BACKGROUNDS = new Set<CodexToolBackground>(["subtle", "status", "none"]);
73
89
 
90
+ type Environment = Readonly<Record<string, string | undefined>>;
91
+
74
92
  function isRecord(value: unknown): value is Record<string, unknown> {
75
93
  return value !== null && typeof value === "object" && !Array.isArray(value);
76
94
  }
77
95
 
96
+ function invalidEnvironmentValue(name: string, value: string, expected: string): never {
97
+ throw new Error(`Invalid ${name}=${JSON.stringify(value)}; expected ${expected}`);
98
+ }
99
+
100
+ function environmentBoolean(environment: Environment, name: string): boolean | undefined {
101
+ const raw = environment[name];
102
+ if (raw === undefined) return undefined;
103
+
104
+ switch (raw.trim().toLowerCase()) {
105
+ case "1":
106
+ case "enabled":
107
+ case "on":
108
+ case "true":
109
+ return true;
110
+ case "0":
111
+ case "disabled":
112
+ case "off":
113
+ case "false":
114
+ return false;
115
+ default:
116
+ return invalidEnvironmentValue(name, raw, "true, false, 1, 0, on, off, enabled, or disabled");
117
+ }
118
+ }
119
+
120
+ function environmentEnum<T extends string>(
121
+ environment: Environment,
122
+ name: string,
123
+ values: ReadonlySet<T>,
124
+ ): T | undefined {
125
+ const raw = environment[name];
126
+ if (raw === undefined) return undefined;
127
+
128
+ const value = raw.trim();
129
+ if (values.has(value as T)) return value as T;
130
+ return invalidEnvironmentValue(name, raw, [...values].join(", "));
131
+ }
132
+
133
+ /**
134
+ * Parse explicit process-level overrides. Unlike invalid JSON settings, invalid
135
+ * environment values fail fast so a CLI test cannot silently exercise a
136
+ * different configuration than requested.
137
+ */
138
+ export function parseEnvironmentConfig(environment: Environment = process.env): ConfigLayer {
139
+ const layer: ConfigLayer = {};
140
+
141
+ const fastMode = environmentBoolean(environment, CONFIG_ENVIRONMENT_VARIABLES.fastMode);
142
+ if (fastMode !== undefined) layer.fastMode = fastMode;
143
+
144
+ const applyPatch = environmentBoolean(environment, CONFIG_ENVIRONMENT_VARIABLES.applyPatch);
145
+ if (applyPatch !== undefined) layer.applyPatch = applyPatch;
146
+
147
+ const toolBackground = environmentEnum(
148
+ environment,
149
+ CONFIG_ENVIRONMENT_VARIABLES.toolBackground,
150
+ CODEX_TOOL_BACKGROUNDS,
151
+ );
152
+ if (toolBackground !== undefined) layer.toolBackground = toolBackground;
153
+
154
+ const imageGeneration = environmentBoolean(
155
+ environment,
156
+ CONFIG_ENVIRONMENT_VARIABLES.imageGeneration,
157
+ );
158
+ if (imageGeneration !== undefined) layer.imageGeneration = imageGeneration;
159
+
160
+ const imageDetail = environmentEnum(
161
+ environment,
162
+ CONFIG_ENVIRONMENT_VARIABLES.imageDetail,
163
+ IMAGE_DETAILS,
164
+ );
165
+ if (imageDetail !== undefined) layer.imageDetail = imageDetail;
166
+
167
+ const webRun = environmentBoolean(environment, CONFIG_ENVIRONMENT_VARIABLES.webRun);
168
+ if (webRun !== undefined) layer.webRun = webRun;
169
+
170
+ const thresholdName = CONFIG_ENVIRONMENT_VARIABLES.autoCompactAtPercent;
171
+ const rawThreshold = environment[thresholdName];
172
+ if (rawThreshold !== undefined) {
173
+ const value = rawThreshold.trim();
174
+ if (value.toLowerCase() === "off" || value.toLowerCase() === "default") {
175
+ layer.autoCompactAtPercent = null;
176
+ } else {
177
+ const threshold = Number(value);
178
+ if (value.length === 0 || !Number.isFinite(threshold) || threshold <= 0 || threshold > 100) {
179
+ invalidEnvironmentValue(
180
+ thresholdName,
181
+ rawThreshold,
182
+ "a number greater than 0 and at most 100, off, or default",
183
+ );
184
+ }
185
+ layer.autoCompactAtPercent = threshold;
186
+ }
187
+ }
188
+
189
+ const webSearch = environmentEnum(
190
+ environment,
191
+ CONFIG_ENVIRONMENT_VARIABLES.webSearch,
192
+ WEB_SEARCH_MODES,
193
+ );
194
+ if (webSearch !== undefined) layer.webSearch = webSearch;
195
+
196
+ const textVerbosity = environmentEnum(
197
+ environment,
198
+ CONFIG_ENVIRONMENT_VARIABLES.textVerbosity,
199
+ TEXT_VERBOSITIES,
200
+ );
201
+ if (textVerbosity !== undefined) layer.textVerbosity = textVerbosity;
202
+
203
+ const reasoningSummary = environmentEnum(
204
+ environment,
205
+ CONFIG_ENVIRONMENT_VARIABLES.reasoningSummary,
206
+ REASONING_SUMMARIES,
207
+ );
208
+ if (reasoningSummary !== undefined) layer.reasoningSummary = reasoningSummary;
209
+
210
+ const reasoningMode = environmentEnum(
211
+ environment,
212
+ CONFIG_ENVIRONMENT_VARIABLES.reasoningMode,
213
+ REASONING_MODES,
214
+ );
215
+ if (reasoningMode !== undefined) layer.reasoningMode = reasoningMode;
216
+
217
+ return layer;
218
+ }
219
+
78
220
  export function parseConfig(value: unknown): ConfigLayer {
79
221
  if (!isRecord(value)) return {};
80
222
 
@@ -157,8 +299,9 @@ function readConfig(filePath: string): ConfigLayer {
157
299
  export function resolveConfig(
158
300
  globalConfig: ConfigLayer,
159
301
  projectConfig: ConfigLayer,
302
+ environmentConfig: ConfigLayer = {},
160
303
  ): CodexCompatConfig {
161
- const merged = { ...globalConfig, ...projectConfig };
304
+ const merged = { ...globalConfig, ...projectConfig, ...environmentConfig };
162
305
  return {
163
306
  ...DEFAULT_CONFIG,
164
307
  ...(typeof merged.fastMode === "boolean" ? { fastMode: merged.fastMode } : {}),
@@ -193,10 +336,14 @@ export function writableConfigPath(cwd: string, projectTrusted: boolean): string
193
336
  return projectTrusted && existsSync(projectPath) ? projectPath : globalConfigPath();
194
337
  }
195
338
 
196
- export function loadConfig(cwd: string, projectTrusted: boolean): CodexCompatConfig {
339
+ export function loadConfig(
340
+ cwd: string,
341
+ projectTrusted: boolean,
342
+ environment: Environment = process.env,
343
+ ): CodexCompatConfig {
197
344
  const globalConfig = readConfig(globalConfigPath());
198
345
  const projectConfig = projectTrusted ? readConfig(projectConfigPath(cwd)) : {};
199
- return resolveConfig(globalConfig, projectConfig);
346
+ return resolveConfig(globalConfig, projectConfig, parseEnvironmentConfig(environment));
200
347
  }
201
348
 
202
349
  export function configLayer(config: CodexCompatConfig): ConfigLayer {
@@ -215,6 +362,18 @@ export function configLayer(config: CodexCompatConfig): ConfigLayer {
215
362
  };
216
363
  }
217
364
 
365
+ /** Keep process-level overrides transient when an effective config is saved. */
366
+ export function withoutEnvironmentOverrides(
367
+ layer: ConfigLayer,
368
+ environmentConfig: ConfigLayer,
369
+ ): ConfigLayer {
370
+ const result = { ...layer };
371
+ for (const key of Object.keys(environmentConfig) as (keyof ConfigLayer)[]) {
372
+ delete result[key];
373
+ }
374
+ return result;
375
+ }
376
+
218
377
  async function readWritableConfig(filePath: string): Promise<Record<string, unknown>> {
219
378
  try {
220
379
  const value = JSON.parse(await readFile(filePath, "utf8")) as unknown;
@@ -0,0 +1,37 @@
1
+ import { Type } from "typebox";
2
+
3
+ export const MAX_EDIT_IMAGES = 5;
4
+
5
+ export type ImageGenerationParameters = {
6
+ prompt: string;
7
+ referenced_image_paths?: string[] | null;
8
+ num_last_images_to_include?: number | null;
9
+ };
10
+
11
+ const REFERENCED_IMAGE_PATH_DESCRIPTION =
12
+ "Absolute path to a local PNG, JPEG, GIF, or WebP image to include in an edit. Convert relative paths to absolute paths before calling the tool; the file must exist and be readable.";
13
+
14
+ /**
15
+ * Server-reserved image-generation schema. Range and selector constraints are
16
+ * enforced by the executor because OpenAI rejects additional schema keywords.
17
+ */
18
+ export const IMAGE_GENERATION_PARAMETERS = Type.Unsafe<ImageGenerationParameters>({
19
+ type: "object",
20
+ properties: {
21
+ num_last_images_to_include: {
22
+ type: ["integer", "null"],
23
+ },
24
+ prompt: {
25
+ type: "string",
26
+ },
27
+ referenced_image_paths: {
28
+ type: ["array", "null"],
29
+ items: {
30
+ type: "string",
31
+ description: REFERENCED_IMAGE_PATH_DESCRIPTION,
32
+ },
33
+ },
34
+ },
35
+ required: ["prompt"],
36
+ additionalProperties: false,
37
+ });
@@ -1,17 +1,21 @@
1
1
  import { readFile, mkdir, writeFile } from "node:fs/promises";
2
- import { dirname, isAbsolute, join } from "node:path";
2
+ import { dirname, isAbsolute, join, normalize } from "node:path";
3
3
  import {
4
4
  getAgentDir,
5
5
  type ExtensionAPI,
6
6
  type ExtensionContext,
7
7
  } from "@earendil-works/pi-coding-agent";
8
8
  import type { Model } from "@earendil-works/pi-ai";
9
- import { Type } from "typebox";
10
9
  import type { CodexCompatConfig } from "./config.ts";
11
10
  import type { CodexToolBackgroundResolver } from "./codex-tool-surface.ts";
12
11
  import { DEFAULT_CONFIG } from "./config.ts";
13
12
  import { isObject, type JsonRecord, type ResponsesItem } from "./codex-protocol.ts";
14
13
  import { requestCodexJson, type CodexJsonRequestOptions } from "./codex-transport.ts";
14
+ import {
15
+ IMAGE_GENERATION_PARAMETERS,
16
+ MAX_EDIT_IMAGES,
17
+ type ImageGenerationParameters,
18
+ } from "./image-generation-schema.ts";
15
19
  import { IMAGE_GENERATION_TOOL_NAME } from "./namespaced-tools.ts";
16
20
  import {
17
21
  renderImageGenerationCall,
@@ -22,55 +26,25 @@ import { isCodexModel } from "./request-options.ts";
22
26
  import { codexToolAuthentication, codexToolHistory } from "./tool-runtime.ts";
23
27
 
24
28
  const IMAGE_MODEL = "gpt-image-2";
25
- const MAX_EDIT_IMAGES = 5;
26
29
  const GENERATION_ENDPOINT = "images/generations";
27
30
  const EDIT_ENDPOINT = "images/edits";
28
31
  const GENERATED_IMAGES_DIRECTORY = "generated_images";
29
32
 
30
- const IMAGE_GENERATION_DESCRIPTION = `The \`image_gen.imagegen\` tool enables image generation from descriptions and editing of existing images based on specific instructions. Use it when:
33
+ const IMAGE_GENERATION_DESCRIPTION = `The \`image_gen.imagegen\` tool generates new images from descriptions and edits existing images according to specific instructions. Use it when:
31
34
 
32
35
  - The user requests an image based on a scene description, such as a diagram, portrait, comic, meme, or any other visual.
33
- - The user wants to modify an attached or previously generated image with specific changes, including adding or removing elements, altering colors, improving quality/resolution, or transforming the style (e.g., cartoon, oil painting).
36
+ - The user wants to modify a local, attached, or previously generated image by adding or removing elements, changing colors, improving quality or resolution, or transforming its style.
34
37
 
35
38
  Guidelines:
36
- - imagegen needs a few minutes to finish. In code-mode, use the first-line @exec directive to give the initial call 120 seconds and the same yield for any waits that follow. Once it finishes, return the image with generatedImage(result).
39
+ - Call \`image_gen.imagegen\` directly without reconfirmation unless required source images are unavailable.
37
40
  - Omit both \`referenced_image_paths\` and \`num_last_images_to_include\` when generating a brand new image.
38
- - For edits, use \`referenced_image_paths\` when every target image has a local file path.
39
- - If you have not seen a local image yet, use \`view_image\` to inspect it before editing.
40
- - Use \`num_last_images_to_include\` only when at least one target image has no local file path.
41
- - Set \`num_last_images_to_include\` to the smallest number of recent conversation images that includes every target image, up to 5.
41
+ - For edits, use \`referenced_image_paths\` when every target image has an absolute local path, with at most 5 paths. Use \`read\` first when you need to inspect a local image.
42
+ - Use \`num_last_images_to_include\` only when at least one target image has no local path, and set it to the smallest number of recent conversation images that includes every target, up to 5.
42
43
  - Never provide both \`referenced_image_paths\` and \`num_last_images_to_include\`.
43
44
  - If neither mechanism can include every target image, ask the user to attach the missing images again.
44
- - Directly generate the image without reconfirmation or clarification unless required images must be attached again.
45
- - Always use this tool for image editing unless the user explicitly requests otherwise. Do not use the \`python\` tool for image editing unless specifically instructed.
45
+ - Generated images are returned, displayed, and saved automatically. Do not embed the image in the final response unless the user asks.
46
46
  `;
47
47
 
48
- const imageGenerationParameters = Type.Unsafe<{
49
- prompt: string;
50
- referenced_image_paths?: string[] | null;
51
- num_last_images_to_include?: number | null;
52
- }>({
53
- type: "object",
54
- properties: {
55
- num_last_images_to_include: {
56
- type: ["integer", "null"],
57
- },
58
- prompt: {
59
- type: "string",
60
- },
61
- referenced_image_paths: {
62
- type: ["array", "null"],
63
- items: {
64
- type: "string",
65
- description:
66
- "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
67
- },
68
- },
69
- },
70
- required: ["prompt"],
71
- additionalProperties: false,
72
- });
73
-
74
48
  export type { ImageGenerationDetails } from "./image-generation-render.ts";
75
49
 
76
50
  type JsonRequester = (
@@ -137,12 +111,12 @@ export function recentImageUrls(history: readonly ResponsesItem[], count: number
137
111
  return newestFirst.reverse();
138
112
  }
139
113
 
140
- function normalizeImagePath(path: string): string {
141
- const normalized = path.startsWith("@") ? path.slice(1) : path;
142
- if (!isAbsolute(normalized)) {
114
+ export function normalizeImagePath(path: string): string {
115
+ const unprefixed = path.startsWith("@") ? path.slice(1) : path;
116
+ if (!isAbsolute(unprefixed)) {
143
117
  throw new Error(`referenced image path must be absolute: ${path}`);
144
118
  }
145
- return normalized;
119
+ return normalize(unprefixed);
146
120
  }
147
121
 
148
122
  function imageMimeType(bytes: Uint8Array, path: string): string {
@@ -176,11 +150,7 @@ async function localImageUrl(path: string): Promise<string> {
176
150
  }
177
151
 
178
152
  async function imageRequest(
179
- params: {
180
- prompt: string;
181
- referenced_image_paths?: string[] | null;
182
- num_last_images_to_include?: number | null;
183
- },
153
+ params: ImageGenerationParameters,
184
154
  history: readonly ResponsesItem[],
185
155
  ): Promise<ImageRequest> {
186
156
  const paths = params.referenced_image_paths ?? [];
@@ -290,7 +260,14 @@ export default function registerImageGeneration(
290
260
  name: IMAGE_GENERATION_TOOL_NAME,
291
261
  label: IMAGE_GENERATION_TOOL_NAME,
292
262
  description: IMAGE_GENERATION_DESCRIPTION,
293
- parameters: imageGenerationParameters,
263
+ promptSnippet: "Generate new images or edit existing images",
264
+ promptGuidelines: [
265
+ "Use image_gen.imagegen directly to generate new images or edit existing images without reconfirmation unless required source images are unavailable.",
266
+ "For new images, call image_gen.imagegen without referenced_image_paths or num_last_images_to_include.",
267
+ "For image_gen.imagegen edits, use up to five absolute referenced_image_paths when every target is local; otherwise use num_last_images_to_include from 1 to 5, and use read when you need to inspect a local image.",
268
+ "Never pass both image selectors to image_gen.imagegen; ask the user to reattach images when every target cannot be referenced.",
269
+ ],
270
+ parameters: IMAGE_GENERATION_PARAMETERS,
294
271
  executionMode: "sequential",
295
272
  renderShell: "self",
296
273
  async execute(toolCallId, params, signal, onUpdate, ctx) {
@@ -13,6 +13,13 @@ import registerCodexRequestOptions from "./request-options.ts";
13
13
  import registerCodexSettings from "./settings-pane.ts";
14
14
  import registerCodexTools, { syncCodexTools } from "./tools.ts";
15
15
 
16
+ export {
17
+ closeOpenAICodexWebSocketSessions,
18
+ getOpenAICodexWebSocketDebugStats,
19
+ resetOpenAICodexWebSocketDebugStats,
20
+ type OpenAICodexWebSocketDebugStats,
21
+ } from "./codex-transport.ts";
22
+
16
23
  const DISPLAY_NAME = "OpenAI Codex Compat";
17
24
 
18
25
  function settingsSummary(ctx: ExtensionContext, config: CodexCompatConfig): string {
@@ -30,7 +37,7 @@ function settingsSummary(ctx: ExtensionContext, config: CodexCompatConfig): stri
30
37
  `reasoning summary: ${config.reasoningSummary}`,
31
38
  `auto-compact threshold: ${config.autoCompactAtPercent ?? "Pi default"}`,
32
39
  `save target: ${writableConfigPath(ctx.cwd, ctx.isProjectTrusted())}`,
33
- "settings: /codex-settings (session-only until Ctrl+S)",
40
+ "settings: /codex-settings (Enter saves & closes; Esc discards; Ctrl+S saves)",
34
41
  ].join("\n");
35
42
  }
36
43
 
@@ -0,0 +1,79 @@
1
+ const MAX_PROVIDER_ERROR_BODY_CHARS = 4_000;
2
+
3
+ type ProviderErrorShape = Error & {
4
+ statusCode?: unknown;
5
+ status?: unknown;
6
+ body?: unknown;
7
+ error?: unknown;
8
+ $metadata?: { httpStatusCode?: unknown };
9
+ $response?: { statusCode?: unknown; body?: unknown };
10
+ };
11
+
12
+ function safeJsonStringify(value: unknown): string {
13
+ try {
14
+ const serialized = JSON.stringify(value);
15
+ return serialized === undefined ? String(value) : serialized;
16
+ } catch {
17
+ return String(value);
18
+ }
19
+ }
20
+
21
+ function truncateErrorText(text: string): string {
22
+ return text.length <= MAX_PROVIDER_ERROR_BODY_CHARS
23
+ ? text
24
+ : `${text.slice(0, MAX_PROVIDER_ERROR_BODY_CHARS)}... [truncated ${
25
+ text.length - MAX_PROVIDER_ERROR_BODY_CHARS
26
+ } chars]`;
27
+ }
28
+
29
+ function isPlainNonEmptyObject(value: unknown): value is Record<string, unknown> {
30
+ if (typeof value !== "object" || value === null) return false;
31
+ const prototype = Object.getPrototypeOf(value);
32
+ return (prototype === Object.prototype || prototype === null) && Object.keys(value).length > 0;
33
+ }
34
+
35
+ function isReadableStreamLike(value: unknown): boolean {
36
+ return (
37
+ typeof value === "object" &&
38
+ value !== null &&
39
+ "pipe" in value &&
40
+ typeof value.pipe === "function"
41
+ );
42
+ }
43
+
44
+ function errorStatus(error: ProviderErrorShape): number | undefined {
45
+ if (typeof error.statusCode === "number") return error.statusCode;
46
+ if (typeof error.status === "number") return error.status;
47
+ if (typeof error.$metadata?.httpStatusCode === "number") {
48
+ return error.$metadata.httpStatusCode;
49
+ }
50
+ if (typeof error.$response?.statusCode === "number") return error.$response.statusCode;
51
+ return undefined;
52
+ }
53
+
54
+ function errorBody(error: ProviderErrorShape): string | undefined {
55
+ let body: string | undefined;
56
+ if (typeof error.body === "string") body = error.body;
57
+ else if (isPlainNonEmptyObject(error.error)) body = safeJsonStringify(error.error);
58
+ else if (typeof error.$response?.body === "string") body = error.$response.body;
59
+ else if (
60
+ !isReadableStreamLike(error.$response?.body) &&
61
+ isPlainNonEmptyObject(error.$response?.body)
62
+ ) {
63
+ body = safeJsonStringify(error.$response.body);
64
+ }
65
+ const trimmed = body?.trim();
66
+ return trimmed ? truncateErrorText(trimmed) : undefined;
67
+ }
68
+
69
+ /** Match Pi AI's provider error normalization without importing a private package subpath. */
70
+ export function formatProviderError(error: unknown): string {
71
+ if (!(error instanceof Error)) return safeJsonStringify(error);
72
+ const providerError = error as ProviderErrorShape;
73
+ const status = errorStatus(providerError);
74
+ const body = errorBody(providerError);
75
+ if (status !== undefined && body !== undefined && !error.message.includes(body)) {
76
+ return `${status}: ${body}`;
77
+ }
78
+ return error.message;
79
+ }
@@ -24,10 +24,3 @@ export function normalizeReplayItem(item: JsonRecord): JsonRecord {
24
24
  }
25
25
  return normalized;
26
26
  }
27
-
28
- export function replayItemsEqual(
29
- left: readonly JsonRecord[] | undefined,
30
- right: readonly JsonRecord[] | undefined,
31
- ): boolean {
32
- return stableResponsesJson(left ?? []) === stableResponsesJson(right ?? []);
33
- }