@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 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;
@@ -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.0",
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.0",
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"
@@ -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["']?/i;
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 { convertMessages as convertCodexResponsesMessages };
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