@gajae-code/ai 0.10.1 → 0.10.2
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,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.10.2] - 2026-07-14
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed frequent `Request blocked (code=invalid_prompt)` failures on gpt-5.6 (Sol/Terra/Luna) subagent, default-agent, and compaction turns (ref openai/codex#32028, oh-my-pi#5184). Leaked Harmony control-token markers (e.g. `<|channel|>analysis`) were only neutralized on the replayed-history payload path, so markers in assistant reasoning summaries, live-converted message/tool-output text, and user-authored content reached the OpenAI Responses and OpenAI-codex-responses transports verbatim and wedged the session (the poisoned item was re-sent every turn). Both transports now neutralize reserved control tokens across the entire outgoing `input` array at the request boundary via an idempotent zero-width-space insertion that keeps the text human-readable.
|
|
10
|
+
|
|
5
11
|
## [0.10.0] - 2026-07-12
|
|
6
12
|
### Fixed
|
|
7
13
|
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -18,6 +18,31 @@ export declare function normalizeResponsesToolCallId(id: string, itemPrefix?: Re
|
|
|
18
18
|
*/
|
|
19
19
|
export declare function truncateResponseItemId(id: string, prefix: string): string;
|
|
20
20
|
export declare function sanitizeOpenAIResponsesHistoryItemsForReplay(items: Array<Record<string, unknown>>): ResponseInput;
|
|
21
|
+
/**
|
|
22
|
+
* Neutralize leaked OpenAI Harmony / control tokens (`<|channel|>`, `<|message|>`,
|
|
23
|
+
* `<|call|>`, `<|constrain|>`, `<|recipient|>`, `<|content|>`, ...) in replayed
|
|
24
|
+
* history text. A subagent whose tool-call channel degenerates can dump raw
|
|
25
|
+
* control-token scaffolding into its reply text; once that poisoned text lands in
|
|
26
|
+
* history the Codex / Responses endpoint rejects every subsequent request with
|
|
27
|
+
* `Request blocked (code=invalid_prompt)`, permanently wedging the session because
|
|
28
|
+
* the offending item is re-sent on each turn. Insert a zero-width space after `<`
|
|
29
|
+
* so the delimiter can no longer be tokenized as a reserved control token while the
|
|
30
|
+
* text stays human-readable.
|
|
31
|
+
*/
|
|
32
|
+
export declare function neutralizeReservedControlTokens(text: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Neutralize leaked reserved control tokens across every string in an outgoing
|
|
35
|
+
* Responses `input` array. This is the request-boundary complement to the
|
|
36
|
+
* replay-history sanitizer: leaked Harmony markers (`<|channel|>analysis`, ...)
|
|
37
|
+
* can enter the payload from assistant reasoning summaries, live-converted
|
|
38
|
+
* message/tool-output text, or user-authored content — not just replayed
|
|
39
|
+
* history — and every gpt-5.6 request that carries one is rejected with
|
|
40
|
+
* `Request blocked (code=invalid_prompt)`. Walking every string (rather than an
|
|
41
|
+
* item-type allowlist) guarantees no leak source is missed as item shapes
|
|
42
|
+
* evolve; the zero-width-space insertion is idempotent (`<\u200b|` no longer
|
|
43
|
+
* matches `<|`) and keeps the text human-readable.
|
|
44
|
+
*/
|
|
45
|
+
export declare function neutralizeResponsesInputControlTokens<T>(items: readonly T[]): T[];
|
|
21
46
|
export declare function createOpenAIResponsesHistoryPayload(provider: string, items: Array<Record<string, unknown>>, incremental?: boolean): OpenAIResponsesHistoryPayload;
|
|
22
47
|
export declare function getOpenAIResponsesHistoryPayload(providerPayload: ProviderPayload | undefined, currentProvider: string, fallbackProvider?: string): OpenAIResponsesHistoryPayload | undefined;
|
|
23
48
|
export declare function getOpenAIResponsesHistoryItems(providerPayload: ProviderPayload | undefined, currentProvider: string, fallbackProvider?: string): Array<Record<string, unknown>> | undefined;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/ai",
|
|
4
|
-
"version": "0.10.
|
|
4
|
+
"version": "0.10.2",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
7
7
|
"author": "Yeachan-Heo and Gajae Code Contributors",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
42
42
|
"@bufbuild/protobuf": "^2.12.0",
|
|
43
|
-
"@gajae-code/utils": "0.10.
|
|
43
|
+
"@gajae-code/utils": "0.10.2",
|
|
44
44
|
"openai": "^6.36.0",
|
|
45
45
|
"partial-json": "^0.1.7",
|
|
46
46
|
"zod": "4.4.3"
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
createOpenAIResponsesHistoryPayload,
|
|
44
44
|
getOpenAIResponsesHistoryItems,
|
|
45
45
|
getOpenAIResponsesHistoryPayload,
|
|
46
|
+
neutralizeResponsesInputControlTokens,
|
|
46
47
|
normalizeSystemPrompts,
|
|
47
48
|
sanitizeOpenAIResponsesHistoryItemsForReplay,
|
|
48
49
|
} from "../utils";
|
|
@@ -633,7 +634,7 @@ async function buildTransformedCodexRequestBody(
|
|
|
633
634
|
): Promise<RequestBody> {
|
|
634
635
|
const params: RequestBody = {
|
|
635
636
|
model: model.id,
|
|
636
|
-
input:
|
|
637
|
+
input: neutralizeResponsesInputControlTokens(convertMessages(model, context)),
|
|
637
638
|
stream: true,
|
|
638
639
|
prompt_cache_key: normalizeOpenAIResponsesPromptCacheKey(options?.sessionId),
|
|
639
640
|
};
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
createOpenAIResponsesHistoryPayload,
|
|
34
34
|
getOpenAIResponsesHistoryItems,
|
|
35
35
|
getOpenAIResponsesHistoryPayload,
|
|
36
|
+
neutralizeResponsesInputControlTokens,
|
|
36
37
|
normalizeSystemPrompts,
|
|
37
38
|
resolveCacheRetention,
|
|
38
39
|
sanitizeOpenAIResponsesHistoryItemsForReplay,
|
|
@@ -493,7 +494,7 @@ function buildParams(
|
|
|
493
494
|
strictResponsesPairing,
|
|
494
495
|
providerSessionState,
|
|
495
496
|
);
|
|
496
|
-
const messages: ResponseInput =
|
|
497
|
+
const messages: ResponseInput = neutralizeResponsesInputControlTokens(conversationMessages);
|
|
497
498
|
|
|
498
499
|
const systemPrompts = normalizeSystemPrompts(context.systemPrompt);
|
|
499
500
|
if (isComposerHarnessModel(model.id)) {
|
package/src/utils.ts
CHANGED
|
@@ -119,22 +119,68 @@ export function sanitizeOpenAIResponsesHistoryItemsForReplay(items: Array<Record
|
|
|
119
119
|
return sanitized ? [sanitized] : [];
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
|
+
const RESERVED_CONTROL_TOKEN_RE = /<\|(?=[A-Za-z0-9_]{1,32}\|>)/g;
|
|
123
|
+
/**
|
|
124
|
+
* Neutralize leaked OpenAI Harmony / control tokens (`<|channel|>`, `<|message|>`,
|
|
125
|
+
* `<|call|>`, `<|constrain|>`, `<|recipient|>`, `<|content|>`, ...) in replayed
|
|
126
|
+
* history text. A subagent whose tool-call channel degenerates can dump raw
|
|
127
|
+
* control-token scaffolding into its reply text; once that poisoned text lands in
|
|
128
|
+
* history the Codex / Responses endpoint rejects every subsequent request with
|
|
129
|
+
* `Request blocked (code=invalid_prompt)`, permanently wedging the session because
|
|
130
|
+
* the offending item is re-sent on each turn. Insert a zero-width space after `<`
|
|
131
|
+
* so the delimiter can no longer be tokenized as a reserved control token while the
|
|
132
|
+
* text stays human-readable.
|
|
133
|
+
*/
|
|
134
|
+
export function neutralizeReservedControlTokens(text: string): string {
|
|
135
|
+
if (!text.includes("<|")) return text;
|
|
136
|
+
return text.replace(RESERVED_CONTROL_TOKEN_RE, "<\u200b|");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Neutralize leaked reserved control tokens across every string in an outgoing
|
|
141
|
+
* Responses `input` array. This is the request-boundary complement to the
|
|
142
|
+
* replay-history sanitizer: leaked Harmony markers (`<|channel|>analysis`, ...)
|
|
143
|
+
* can enter the payload from assistant reasoning summaries, live-converted
|
|
144
|
+
* message/tool-output text, or user-authored content — not just replayed
|
|
145
|
+
* history — and every gpt-5.6 request that carries one is rejected with
|
|
146
|
+
* `Request blocked (code=invalid_prompt)`. Walking every string (rather than an
|
|
147
|
+
* item-type allowlist) guarantees no leak source is missed as item shapes
|
|
148
|
+
* evolve; the zero-width-space insertion is idempotent (`<\u200b|` no longer
|
|
149
|
+
* matches `<|`) and keeps the text human-readable.
|
|
150
|
+
*/
|
|
151
|
+
export function neutralizeResponsesInputControlTokens<T>(items: readonly T[]): T[] {
|
|
152
|
+
return items.map(item => deepNeutralizeReservedControlTokens(item) as T);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function deepNeutralizeReservedControlTokens(value: unknown): unknown {
|
|
156
|
+
if (typeof value === "string") return neutralizeReservedControlTokens(value);
|
|
157
|
+
if (Array.isArray(value)) return value.map(deepNeutralizeReservedControlTokens);
|
|
158
|
+
if (value && typeof value === "object") {
|
|
159
|
+
const out: Record<string, unknown> = {};
|
|
160
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
161
|
+
out[key] = deepNeutralizeReservedControlTokens(nested);
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
|
|
122
168
|
function stringifyResponsesStringParamForReplay(value: unknown): string {
|
|
123
|
-
if (typeof value === "string") return value.toWellFormed();
|
|
169
|
+
if (typeof value === "string") return neutralizeReservedControlTokens(value.toWellFormed());
|
|
124
170
|
try {
|
|
125
171
|
const encoded = JSON.stringify(value);
|
|
126
|
-
if (typeof encoded === "string") return encoded.toWellFormed();
|
|
172
|
+
if (typeof encoded === "string") return neutralizeReservedControlTokens(encoded.toWellFormed());
|
|
127
173
|
} catch {
|
|
128
174
|
// Fall through to String().
|
|
129
175
|
}
|
|
130
|
-
return String(value ?? "").toWellFormed();
|
|
176
|
+
return neutralizeReservedControlTokens(String(value ?? "").toWellFormed());
|
|
131
177
|
}
|
|
132
178
|
|
|
133
179
|
function normalizeResponsesMessageTextForReplay(value: unknown): string {
|
|
134
|
-
if (typeof value === "string") return value.toWellFormed();
|
|
180
|
+
if (typeof value === "string") return neutralizeReservedControlTokens(value.toWellFormed());
|
|
135
181
|
if (value && typeof value === "object") {
|
|
136
182
|
const nestedText = (value as { text?: unknown }).text;
|
|
137
|
-
if (typeof nestedText === "string") return nestedText.toWellFormed();
|
|
183
|
+
if (typeof nestedText === "string") return neutralizeReservedControlTokens(nestedText.toWellFormed());
|
|
138
184
|
}
|
|
139
185
|
return stringifyResponsesStringParamForReplay(value);
|
|
140
186
|
}
|
|
@@ -163,7 +209,7 @@ function normalizeResponsesImageUrlForReplay(value: unknown): NormalizedResponse
|
|
|
163
209
|
}
|
|
164
210
|
|
|
165
211
|
function sanitizeResponsesMessageContentForReplay(content: unknown): unknown {
|
|
166
|
-
if (typeof content === "string") return content.toWellFormed();
|
|
212
|
+
if (typeof content === "string") return neutralizeReservedControlTokens(content.toWellFormed());
|
|
167
213
|
if (!Array.isArray(content)) return content;
|
|
168
214
|
return content.map(part => {
|
|
169
215
|
if (!part || typeof part !== "object") return part;
|
|
@@ -197,12 +243,11 @@ function sanitizeResponsesStringFieldsForReplay(item: Record<string, unknown>):
|
|
|
197
243
|
if (item.type === "custom_tool_call" && "input" in item && typeof item.input !== "string") {
|
|
198
244
|
item.input = stringifyResponsesStringParamForReplay(item.input);
|
|
199
245
|
}
|
|
200
|
-
if (
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
item.output = stringifyResponsesStringParamForReplay(item.output);
|
|
246
|
+
if ((item.type === "function_call_output" || item.type === "custom_tool_call_output") && "output" in item) {
|
|
247
|
+
item.output =
|
|
248
|
+
typeof item.output === "string"
|
|
249
|
+
? neutralizeReservedControlTokens(item.output.toWellFormed())
|
|
250
|
+
: stringifyResponsesStringParamForReplay(item.output);
|
|
206
251
|
}
|
|
207
252
|
}
|
|
208
253
|
|