@gajae-code/ai 0.11.0 → 0.11.1
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 +2 -0
- package/dist/types/model-cache.d.ts +2 -0
- package/dist/types/providers/openai-codex-responses.d.ts +3 -2
- package/dist/types/utils.d.ts +12 -0
- package/package.json +2 -2
- package/src/model-cache.ts +10 -0
- package/src/providers/openai-codex-responses.ts +12 -3
- package/src/providers/openai-responses.ts +17 -0
- package/src/utils.ts +45 -0
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
- Closed the remaining `Request blocked (code=invalid_prompt)` wedge on gpt-5.6 caused by header-form leaked Harmony markers. The reserved-control-token sanitizer only matched the simple `<|ident|>` shape, so a header-form marker carrying a recipient (e.g. `<|assistant to=functions.bash|>`) survived every sanitizer path (replay, request boundary, compaction) and kept re-poisoning history even after the earlier fixes. The pattern now also matches the scoped header grammar — a known Harmony role (`system`/`developer`/`user`/`assistant`/`tool`) plus a `to=<recipient>` assignment with unbounded recipient length — while leaving ordinary delimiter/pipe text untouched (arbitrary `<|foo bar=baz|>`, F# `value <| f |> g`, compact `sum<|a+b|>c`, and multi-line bodies never match). The simple branch remains a strict superset of the prior identifier-only pattern (#2267).
|
|
15
15
|
|
|
16
|
+
- Made the `Request blocked (code=invalid_prompt)` classification explicit and shared across transports (#2282). `invalid_prompt` was only non-retryable by omission — it appeared in neither the codex retryable nor non-retryable event set, and the plain OpenAI Responses transport surfaced it as a generic error with no durable marker. It is now in the codex `CODEX_NON_RETRYABLE_EVENT_CODES` set (code and message forms), the Responses error path tags `transportFailure.providerCode = "invalid_prompt"`, and a new exported `isInvalidPromptError` predicate is the single contract both transports and the session-level circuit breaker key on. Ordinary control-token / pipe text (F# `value <| f |> g`, `sum<|a+b|>c`, `<|foo bar=baz|>`) is unaffected; genuinely transient errors (`server_error`, `model_error`) stay retryable.
|
|
17
|
+
|
|
16
18
|
## [0.10.2] - 2026-07-14
|
|
17
19
|
|
|
18
20
|
### Fixed
|
|
@@ -12,6 +12,8 @@ interface CacheEntry<TApi extends Api = Api> {
|
|
|
12
12
|
*/
|
|
13
13
|
staticFingerprint: string;
|
|
14
14
|
}
|
|
15
|
+
/** Close the shared cache only when it owns the exact requested database path. */
|
|
16
|
+
export declare function closeModelCache(dbPath?: string): boolean;
|
|
15
17
|
export declare function readModelCache<TApi extends Api>(providerId: string, ttlMs: number, now: () => number, dbPath?: string): CacheEntry<TApi> | null;
|
|
16
18
|
export declare function writeModelCache<TApi extends Api>(providerId: string, updatedAt: number, models: Model<TApi>[], authoritative: boolean, staticFingerprint: string, dbPath?: string): void;
|
|
17
19
|
export {};
|
|
@@ -45,8 +45,8 @@ export declare function getOpenAICodexTransportDetails(model: Model<"openai-code
|
|
|
45
45
|
providerSessionState?: Map<string, ProviderSessionState>;
|
|
46
46
|
}): OpenAICodexTransportDetails;
|
|
47
47
|
declare function convertMessages(model: Model<"openai-codex-responses">, context: Context): ResponseInput;
|
|
48
|
-
/** @internal Exported for tests. */
|
|
49
|
-
export { convertMessages as convertCodexResponsesMessages };
|
|
48
|
+
/** @internal Exported for tests. `classifyCodexFailureEventRetryable` is the retry classification of a Codex failure event. */
|
|
49
|
+
export { convertMessages as convertCodexResponsesMessages, isRetryableCodexFailureEvent as classifyCodexFailureEventRetryable, };
|
|
50
50
|
type CodexToolPayload = {
|
|
51
51
|
type: "function";
|
|
52
52
|
name: string;
|
|
@@ -65,3 +65,4 @@ type CodexToolPayload = {
|
|
|
65
65
|
};
|
|
66
66
|
/** @internal Exported for tests. */
|
|
67
67
|
export declare function convertOpenAICodexResponsesTools(tools: Tool[], model: Model<"openai-codex-responses">): CodexToolPayload[];
|
|
68
|
+
declare function isRetryableCodexFailureEvent(rawEvent: Record<string, unknown>): boolean;
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -47,6 +47,18 @@ export declare function sanitizeOpenAIResponsesHistoryItemsForReplay(items: Arra
|
|
|
47
47
|
* every marker the old regex caught still matches.
|
|
48
48
|
*/
|
|
49
49
|
export declare function neutralizeReservedControlTokens(text: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Shape-tolerant classifier for the poisoned-history rejection that wedges
|
|
52
|
+
* gpt-5.6 sessions: `Request blocked (code=invalid_prompt)`. Accepts a raw
|
|
53
|
+
* provider error, an assistant message, or any object carrying a
|
|
54
|
+
* `providerCode` / `transportFailure` / `errorMessage` field, and returns true
|
|
55
|
+
* when the failure is the deterministic `invalid_prompt` content fault rather
|
|
56
|
+
* than a transient upstream error. This is the single shared contract the
|
|
57
|
+
* provider transports and the session-level circuit breaker key on so the
|
|
58
|
+
* classification is explicit (not inferred from a catch-all bucket) and
|
|
59
|
+
* uniformly testable across transports.
|
|
60
|
+
*/
|
|
61
|
+
export declare function isInvalidPromptError(input: unknown): boolean;
|
|
50
62
|
/**
|
|
51
63
|
* Neutralize leaked reserved control tokens across every string in an outgoing
|
|
52
64
|
* Responses `input` array. This is the request-boundary complement to the
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/ai",
|
|
4
|
-
"version": "0.11.
|
|
4
|
+
"version": "0.11.1",
|
|
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.11.
|
|
43
|
+
"@gajae-code/utils": "0.11.1",
|
|
44
44
|
"openai": "^6.36.0",
|
|
45
45
|
"partial-json": "^0.1.7",
|
|
46
46
|
"zod": "4.4.3"
|
package/src/model-cache.ts
CHANGED
|
@@ -66,6 +66,16 @@ function getDb(dbPath?: string): Database {
|
|
|
66
66
|
return db;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/** Close the shared cache only when it owns the exact requested database path. */
|
|
70
|
+
export function closeModelCache(dbPath?: string): boolean {
|
|
71
|
+
const resolvedPath = dbPath ?? getModelDbPath();
|
|
72
|
+
if (!sharedDb || sharedDbPath !== resolvedPath) return false;
|
|
73
|
+
sharedDb.close();
|
|
74
|
+
sharedDb = null;
|
|
75
|
+
sharedDbPath = null;
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
69
79
|
function migrateCacheSchema(db: Database): void {
|
|
70
80
|
const columns = db.prepare("PRAGMA table_info(model_cache)").all() as TableInfoRow[];
|
|
71
81
|
if (!columns.some(column => column.name === "static_fingerprint")) {
|
|
@@ -113,9 +113,15 @@ const CODEX_NON_RETRYABLE_EVENT_CODES = new Set([
|
|
|
113
113
|
"invalid_request_error",
|
|
114
114
|
"invalid_schema",
|
|
115
115
|
"invalid_tool_schema",
|
|
116
|
+
// A poisoned-history rejection (`Request blocked (code=invalid_prompt)`) is a
|
|
117
|
+
// deterministic content fault, not a transient upstream failure: retrying the
|
|
118
|
+
// same request re-sends the same offending item and re-triggers the block, so
|
|
119
|
+
// classify it as explicitly non-retryable instead of relying on omission
|
|
120
|
+
// (the request-boundary sanitizer, not a provider retry, is the recovery path).
|
|
121
|
+
"invalid_prompt",
|
|
116
122
|
]);
|
|
117
123
|
const CODEX_NON_RETRYABLE_EVENT_MESSAGE =
|
|
118
|
-
/invalid[_ -]function[_ -]parameters|invalid schema for function|invalid[_ -]tool[_ -]schema|schema must have type ["']?object["']
|
|
124
|
+
/invalid[_ -]function[_ -]parameters|invalid schema for function|invalid[_ -]tool[_ -]schema|schema must have type ["']?object["']?|request blocked[^\n]*invalid[_ -]prompt|code=invalid[_ -]prompt/i;
|
|
119
125
|
const CODEX_RETRYABLE_EVENT_MESSAGE =
|
|
120
126
|
/processing your request|retry your request|temporar(?:y|ily)|overloaded|service.?unavailable|internal error|server error/i;
|
|
121
127
|
const CODEX_PROVIDER_SESSION_STATE_KEY = "openai-codex-responses";
|
|
@@ -2650,8 +2656,11 @@ function normalizeInputMessageContent(
|
|
|
2650
2656
|
return convertResponsesInputContent(content, model.input.includes("image")) ?? [];
|
|
2651
2657
|
}
|
|
2652
2658
|
|
|
2653
|
-
/** @internal Exported for tests. */
|
|
2654
|
-
export {
|
|
2659
|
+
/** @internal Exported for tests. `classifyCodexFailureEventRetryable` is the retry classification of a Codex failure event. */
|
|
2660
|
+
export {
|
|
2661
|
+
convertMessages as convertCodexResponsesMessages,
|
|
2662
|
+
isRetryableCodexFailureEvent as classifyCodexFailureEventRetryable,
|
|
2663
|
+
};
|
|
2655
2664
|
|
|
2656
2665
|
/**
|
|
2657
2666
|
* Whether this OpenAI code backend-backend model should get the custom-tool grammar
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
createOpenAIResponsesHistoryPayload,
|
|
34
34
|
getOpenAIResponsesHistoryItems,
|
|
35
35
|
getOpenAIResponsesHistoryPayload,
|
|
36
|
+
isInvalidPromptError,
|
|
36
37
|
neutralizeResponsesInputControlTokens,
|
|
37
38
|
normalizeSystemPrompts,
|
|
38
39
|
resolveCacheRetention,
|
|
@@ -387,6 +388,22 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
|
|
|
387
388
|
output.transportFailure = transportFailureFacts(error);
|
|
388
389
|
output.errorMessage = firstEventTimeoutError?.message ?? (await finalizeErrorMessage(error, rawRequestDump));
|
|
389
390
|
output.errorMessage = rewriteCopilotError(output.errorMessage, error, model.provider);
|
|
391
|
+
// Explicitly mark the poisoned-history rejection so the shared
|
|
392
|
+
// `invalid_prompt` contract is present even when the SDK error surfaces
|
|
393
|
+
// only a message (no structured code). This keeps the responses
|
|
394
|
+
// transport's classification uniform with the codex transport's
|
|
395
|
+
// non-retryable event set and lets the session-level circuit breaker
|
|
396
|
+
// key on one durable marker instead of per-transport string matching.
|
|
397
|
+
if (
|
|
398
|
+
output.stopReason === "error" &&
|
|
399
|
+
!output.transportFailure?.providerCode &&
|
|
400
|
+
(isInvalidPromptError(error) || isInvalidPromptError(output.errorMessage))
|
|
401
|
+
) {
|
|
402
|
+
output.transportFailure = {
|
|
403
|
+
...(output.transportFailure ?? { kind: "transport" }),
|
|
404
|
+
providerCode: "invalid_prompt",
|
|
405
|
+
};
|
|
406
|
+
}
|
|
390
407
|
output.duration = Date.now() - startTime;
|
|
391
408
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
392
409
|
stream.push({ type: "error", reason: output.stopReason, error: output });
|
package/src/utils.ts
CHANGED
|
@@ -154,6 +154,51 @@ export function neutralizeReservedControlTokens(text: string): string {
|
|
|
154
154
|
return text.replace(RESERVED_CONTROL_TOKEN_RE, "<\u200b|");
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Shape-tolerant classifier for the poisoned-history rejection that wedges
|
|
159
|
+
* gpt-5.6 sessions: `Request blocked (code=invalid_prompt)`. Accepts a raw
|
|
160
|
+
* provider error, an assistant message, or any object carrying a
|
|
161
|
+
* `providerCode` / `transportFailure` / `errorMessage` field, and returns true
|
|
162
|
+
* when the failure is the deterministic `invalid_prompt` content fault rather
|
|
163
|
+
* than a transient upstream error. This is the single shared contract the
|
|
164
|
+
* provider transports and the session-level circuit breaker key on so the
|
|
165
|
+
* classification is explicit (not inferred from a catch-all bucket) and
|
|
166
|
+
* uniformly testable across transports.
|
|
167
|
+
*/
|
|
168
|
+
export function isInvalidPromptError(input: unknown): boolean {
|
|
169
|
+
if (!input) return false;
|
|
170
|
+
if (typeof input === "string") return INVALID_PROMPT_MESSAGE_RE.test(input);
|
|
171
|
+
if (typeof input !== "object") return false;
|
|
172
|
+
const value = input as {
|
|
173
|
+
providerCode?: unknown;
|
|
174
|
+
code?: unknown;
|
|
175
|
+
errorMessage?: unknown;
|
|
176
|
+
message?: unknown;
|
|
177
|
+
transportFailure?: { providerCode?: unknown; code?: unknown };
|
|
178
|
+
error?: { code?: unknown };
|
|
179
|
+
};
|
|
180
|
+
const code =
|
|
181
|
+
asLowerString(value.providerCode) ??
|
|
182
|
+
asLowerString(value.code) ??
|
|
183
|
+
asLowerString(value.transportFailure?.providerCode) ??
|
|
184
|
+
asLowerString(value.transportFailure?.code) ??
|
|
185
|
+
asLowerString(value.error?.code);
|
|
186
|
+
if (code === "invalid_prompt") return true;
|
|
187
|
+
const message =
|
|
188
|
+
typeof value.errorMessage === "string"
|
|
189
|
+
? value.errorMessage
|
|
190
|
+
: typeof value.message === "string"
|
|
191
|
+
? value.message
|
|
192
|
+
: undefined;
|
|
193
|
+
return message !== undefined && INVALID_PROMPT_MESSAGE_RE.test(message);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const INVALID_PROMPT_MESSAGE_RE = /code=invalid[_ -]prompt|request blocked[^\n]*invalid[_ -]prompt/i;
|
|
197
|
+
|
|
198
|
+
function asLowerString(value: unknown): string | undefined {
|
|
199
|
+
return typeof value === "string" ? value.toLowerCase() : undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
157
202
|
/**
|
|
158
203
|
* Neutralize leaked reserved control tokens across every string in an outgoing
|
|
159
204
|
* Responses `input` array. This is the request-boundary complement to the
|