@gajae-code/ai 0.7.4 → 0.7.6

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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.7.5] - 2026-06-27
6
+
7
+ ### Fixed
8
+
9
+ - Sanitized Codex history replay text fields so malformed/non-string replay text no longer corrupts the rebuilt request payload (#1199).
10
+ - Preserved object-valued tool replay fields when rebuilding Codex history payloads instead of coercing them to strings (#1200).
11
+
5
12
  ## [0.7.4] - 2026-06-27
6
13
 
7
14
  ### Fixed
@@ -18,6 +18,7 @@ export interface InputItem {
18
18
  name?: string;
19
19
  output?: unknown;
20
20
  arguments?: unknown;
21
+ encrypted_content?: unknown;
21
22
  }
22
23
  export interface RequestBody {
23
24
  model: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/ai",
4
- "version": "0.7.4",
4
+ "version": "0.7.6",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://gaebal-gajae.dev",
7
7
  "author": "Yeachan-Heo",
@@ -43,7 +43,7 @@
43
43
  "dependencies": {
44
44
  "@anthropic-ai/sdk": "^0.94.0",
45
45
  "@bufbuild/protobuf": "^2.12.0",
46
- "@gajae-code/utils": "0.7.4",
46
+ "@gajae-code/utils": "0.7.6",
47
47
  "openai": "^6.36.0",
48
48
  "partial-json": "^0.1.7",
49
49
  "zod": "4.4.3"
@@ -23,6 +23,7 @@ export interface InputItem {
23
23
  name?: string;
24
24
  output?: unknown;
25
25
  arguments?: unknown;
26
+ encrypted_content?: unknown;
26
27
  }
27
28
 
28
29
  export interface RequestBody {
@@ -62,6 +63,57 @@ function getReasoningConfig(model: Model<Api>, options: CodexRequestOptions): Re
62
63
  return config;
63
64
  }
64
65
 
66
+ function describeTextPartValue(value: unknown): string {
67
+ if (value === null) return "null";
68
+ if (Array.isArray(value)) return "array";
69
+ return typeof value;
70
+ }
71
+
72
+ function normalizeTextPartValue(value: unknown, path: string): string {
73
+ if (typeof value === "string") return value.toWellFormed();
74
+ try {
75
+ const encoded = JSON.stringify(value);
76
+ if (typeof encoded === "string") return encoded.toWellFormed();
77
+ } catch {
78
+ // Fall through to the actionable local error below.
79
+ }
80
+ throw new Error(
81
+ `Invalid Codex request text part at ${path}: expected a string or JSON-serializable value, received ${describeTextPartValue(value)}. Normalize compacted continuation content before sending to Codex.`,
82
+ );
83
+ }
84
+
85
+ function normalizeTextPartFields(content: unknown, path: string): unknown {
86
+ if (typeof content === "string") return content.toWellFormed();
87
+ if (!Array.isArray(content)) return content;
88
+ return content.map((part, index) => {
89
+ if (!part || typeof part !== "object") return part;
90
+ const normalizedPart = { ...(part as Record<string, unknown>) };
91
+ if ("text" in normalizedPart) {
92
+ normalizedPart.text = normalizeTextPartValue(normalizedPart.text, `${path}[${index}].text`);
93
+ }
94
+ return normalizedPart;
95
+ });
96
+ }
97
+
98
+ function normalizeInputTextPartFields(input: InputItem[] | undefined): InputItem[] | undefined {
99
+ if (!Array.isArray(input)) return input;
100
+ return input.map((item, itemIndex) => {
101
+ const normalizedItem = { ...item };
102
+ const itemRecord = normalizedItem as Record<string, unknown>;
103
+ if ("encrypted_content" in itemRecord) {
104
+ if (typeof itemRecord.encrypted_content === "string") {
105
+ itemRecord.encrypted_content = itemRecord.encrypted_content.toWellFormed();
106
+ } else {
107
+ delete itemRecord.encrypted_content;
108
+ }
109
+ }
110
+ if (normalizedItem.type === "message") {
111
+ normalizedItem.content = normalizeTextPartFields(normalizedItem.content, `input[${itemIndex}].content`);
112
+ }
113
+ return normalizedItem;
114
+ });
115
+ }
116
+
65
117
  function filterInput(input: InputItem[] | undefined): InputItem[] | undefined {
66
118
  if (!Array.isArray(input)) return input;
67
119
 
@@ -121,6 +173,7 @@ export async function transformRequestBody(
121
173
  return item;
122
174
  });
123
175
  }
176
+ body.input = normalizeInputTextPartFields(body.input);
124
177
  }
125
178
 
126
179
  if (prompt?.developerMessages && prompt.developerMessages.length > 0 && Array.isArray(body.input)) {
@@ -44,6 +44,7 @@ import {
44
44
  getOpenAIResponsesHistoryItems,
45
45
  getOpenAIResponsesHistoryPayload,
46
46
  normalizeSystemPrompts,
47
+ sanitizeOpenAIResponsesHistoryItemsForReplay,
47
48
  } from "../utils";
48
49
  import { AssistantMessageEventStream } from "../utils/event-stream";
49
50
  import { finalizeErrorMessage, type RawHttpRequestDump } from "../utils/http-inspector";
@@ -2550,17 +2551,16 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
2550
2551
  for (const msg of transformedMessages) {
2551
2552
  if (msg.role === "user" || msg.role === "developer") {
2552
2553
  const providerPayload = (msg as { providerPayload?: AssistantMessage["providerPayload"] }).providerPayload;
2553
- const historyItems = getOpenAIResponsesHistoryItems(providerPayload, model.provider) as
2554
- | Array<ResponseInput[number]>
2555
- | undefined;
2554
+ const historyItems = getOpenAIResponsesHistoryItems(providerPayload, model.provider);
2556
2555
  if (historyItems) {
2557
- for (const item of historyItems) {
2556
+ const sanitizedHistoryItems = sanitizeOpenAIResponsesHistoryItemsForReplay(historyItems);
2557
+ for (const item of sanitizedHistoryItems) {
2558
2558
  const maybe = item as { type?: string; call_id?: string };
2559
2559
  if (maybe.type === "custom_tool_call" && typeof maybe.call_id === "string") {
2560
2560
  customCallIds.add(maybe.call_id);
2561
2561
  }
2562
2562
  }
2563
- messages.push(...historyItems);
2563
+ messages.push(...sanitizedHistoryItems);
2564
2564
  msgIndex += 1;
2565
2565
  continue;
2566
2566
  }
@@ -2579,18 +2579,19 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
2579
2579
  model.provider,
2580
2580
  assistantMsg.provider,
2581
2581
  );
2582
- const historyItems = providerPayload?.items as Array<ResponseInput[number]> | undefined;
2582
+ const historyItems = providerPayload?.items;
2583
2583
  if (historyItems) {
2584
- for (const item of historyItems) {
2584
+ const sanitizedHistoryItems = sanitizeOpenAIResponsesHistoryItemsForReplay(historyItems);
2585
+ for (const item of sanitizedHistoryItems) {
2585
2586
  const maybe = item as { type?: string; call_id?: string };
2586
2587
  if (maybe.type === "custom_tool_call" && typeof maybe.call_id === "string") {
2587
2588
  customCallIds.add(maybe.call_id);
2588
2589
  }
2589
2590
  }
2590
2591
  if (providerPayload?.dt) {
2591
- messages.push(...historyItems);
2592
+ messages.push(...sanitizedHistoryItems);
2592
2593
  } else {
2593
- messages.splice(0, messages.length, ...historyItems);
2594
+ messages.splice(0, messages.length, ...sanitizedHistoryItems);
2594
2595
  // Keep customCallIds from the pre-splice state since historyItems may re-introduce them.
2595
2596
  }
2596
2597
  msgIndex += 1;
package/src/utils.ts CHANGED
@@ -91,6 +91,92 @@ export function sanitizeOpenAIResponsesHistoryItemsForReplay(items: Array<Record
91
91
  return sanitized ? [sanitized] : [];
92
92
  });
93
93
  }
94
+ function stringifyResponsesStringParamForReplay(value: unknown): string {
95
+ if (typeof value === "string") return value.toWellFormed();
96
+ try {
97
+ const encoded = JSON.stringify(value);
98
+ if (typeof encoded === "string") return encoded.toWellFormed();
99
+ } catch {
100
+ // Fall through to String().
101
+ }
102
+ return String(value ?? "").toWellFormed();
103
+ }
104
+
105
+ function normalizeResponsesMessageTextForReplay(value: unknown): string {
106
+ if (typeof value === "string") return value.toWellFormed();
107
+ if (value && typeof value === "object") {
108
+ const nestedText = (value as { text?: unknown }).text;
109
+ if (typeof nestedText === "string") return nestedText.toWellFormed();
110
+ }
111
+ return stringifyResponsesStringParamForReplay(value);
112
+ }
113
+
114
+ type ResponsesImageDetail = "auto" | "low" | "high";
115
+
116
+ interface NormalizedResponsesImageUrl {
117
+ readonly imageUrl: string;
118
+ readonly detail?: ResponsesImageDetail;
119
+ }
120
+
121
+ function isResponsesImageDetail(value: unknown): value is ResponsesImageDetail {
122
+ return value === "auto" || value === "low" || value === "high";
123
+ }
124
+
125
+ function normalizeResponsesImageUrlForReplay(value: unknown): NormalizedResponsesImageUrl {
126
+ if (typeof value === "string") return { imageUrl: value.toWellFormed() };
127
+ if (value && typeof value === "object" && "url" in value && typeof value.url === "string") {
128
+ const detail = "detail" in value && isResponsesImageDetail(value.detail) ? value.detail : undefined;
129
+ return {
130
+ imageUrl: value.url.toWellFormed(),
131
+ ...(detail ? { detail } : {}),
132
+ };
133
+ }
134
+ return { imageUrl: stringifyResponsesStringParamForReplay(value) };
135
+ }
136
+
137
+ function sanitizeResponsesMessageContentForReplay(content: unknown): unknown {
138
+ if (typeof content === "string") return content.toWellFormed();
139
+ if (!Array.isArray(content)) return content;
140
+ return content.map(part => {
141
+ if (!part || typeof part !== "object") return part;
142
+ const sanitizedPart = { ...(part as Record<string, unknown>) };
143
+ if ("text" in sanitizedPart) {
144
+ sanitizedPart.text = normalizeResponsesMessageTextForReplay(sanitizedPart.text);
145
+ }
146
+ if ("image_url" in sanitizedPart) {
147
+ const normalizedImageUrl = normalizeResponsesImageUrlForReplay(sanitizedPart.image_url);
148
+ sanitizedPart.image_url = normalizedImageUrl.imageUrl;
149
+ if (sanitizedPart.type === "image_url") {
150
+ sanitizedPart.type = "input_image";
151
+ }
152
+ if (normalizedImageUrl.detail) {
153
+ sanitizedPart.detail = normalizedImageUrl.detail;
154
+ } else if ("detail" in sanitizedPart && !isResponsesImageDetail(sanitizedPart.detail)) {
155
+ delete sanitizedPart.detail;
156
+ }
157
+ }
158
+ return sanitizedPart;
159
+ });
160
+ }
161
+
162
+ function sanitizeResponsesStringFieldsForReplay(item: Record<string, unknown>): void {
163
+ if (item.type === "message") {
164
+ item.content = sanitizeResponsesMessageContentForReplay(item.content);
165
+ }
166
+ if (item.type === "function_call" && "arguments" in item && typeof item.arguments !== "string") {
167
+ item.arguments = stringifyResponsesStringParamForReplay(item.arguments);
168
+ }
169
+ if (item.type === "custom_tool_call" && "input" in item && typeof item.input !== "string") {
170
+ item.input = stringifyResponsesStringParamForReplay(item.input);
171
+ }
172
+ if (
173
+ (item.type === "function_call_output" || item.type === "custom_tool_call_output") &&
174
+ "output" in item &&
175
+ typeof item.output !== "string"
176
+ ) {
177
+ item.output = stringifyResponsesStringParamForReplay(item.output);
178
+ }
179
+ }
94
180
 
95
181
  function sanitizeOpenAIResponsesHistoryItemForReplay(
96
182
  item: Record<string, unknown>,
@@ -105,6 +191,7 @@ function sanitizeOpenAIResponsesHistoryItemForReplay(
105
191
  if (typeof item.call_id === "string") {
106
192
  sanitizedItem.call_id = normalizeReplayedResponsesHistoryCallId(item.call_id, normalizedCallIds);
107
193
  }
194
+ sanitizeResponsesStringFieldsForReplay(sanitizedItem);
108
195
 
109
196
  return sanitizedItem as unknown as OpenAIResponsesReplayItem;
110
197
  }