@gajae-code/ai 0.12.11 → 0.12.13

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.
@@ -165,7 +165,10 @@ export function resolveToolChoice(
165
165
 
166
166
  /** Detects provider errors indicating forced tool_choice is unsupported. */
167
167
  export function isForcedToolChoiceUnsupportedError(error: unknown, sentForcedToolChoice: boolean): boolean {
168
- if (!sentForcedToolChoice || extractHttpStatusFromError(error) !== 400) return false;
168
+ const status = extractHttpStatusFromError(error);
169
+ if (!sentForcedToolChoice || status !== 400) {
170
+ return false;
171
+ }
169
172
  const message = errorMessage(error);
170
173
  return (
171
174
  // `by <something>` continuations ("not supported by billing") describe a
@@ -179,6 +182,35 @@ export function isForcedToolChoiceUnsupportedError(error: unknown, sentForcedToo
179
182
  /tool[_\s-]?choices?\s+['"`][^'"`\r\n]+['"`]\s+not\s+found\s+in\s+['"`]tools['"`]\s+parameter\b/is.test(message)
180
183
  );
181
184
  }
185
+ /**
186
+ * Detects Codex's statusless SSE rejection for a named function tool choice.
187
+ * This is intentionally separate from the shared HTTP-400 classifier.
188
+ */
189
+ export function isCodexStatuslessNamedToolChoiceNotFoundError(
190
+ error: unknown,
191
+ forcedToolName: string | undefined,
192
+ sentToolNames: readonly string[],
193
+ ): boolean {
194
+ if (
195
+ extractHttpStatusFromError(error) !== undefined ||
196
+ extractProviderErrorCode(error) !== "invalid_request_error" ||
197
+ !forcedToolName
198
+ ) {
199
+ return false;
200
+ }
201
+ const match =
202
+ /^Tool choice '([^']+)' not found in 'tools' parameter\.$/.exec(errorMessage(error)) ??
203
+ /^Codex error event: Tool choice '([^']+)' not found in 'tools' parameter\. \(code=invalid_request_error\)$/.exec(
204
+ errorMessage(error),
205
+ );
206
+ return match?.[1] === forcedToolName && sentToolNames.includes(forcedToolName);
207
+ }
208
+
209
+ function extractProviderErrorCode(error: unknown): string | undefined {
210
+ if (!error || typeof error !== "object") return undefined;
211
+ const code = (error as { code?: unknown }).code;
212
+ return typeof code === "string" ? code : undefined;
213
+ }
182
214
 
183
215
  export type { ToolChoiceCompat, ToolChoiceSupport, ToolChoiceSupportSource } from "../types";
184
216
 
@@ -965,6 +965,11 @@ const RAW_ARGUMENT_REJECTION_MESSAGES: Record<RawArgumentRejectionCode, string>
965
965
  "deepInterview.intent_contract requires non-empty items and confirmation_options",
966
966
  "ask-deep-interview-metadata-requires-deep-interview-gate":
967
967
  "deepInterview metadata cannot be combined with a non-deep-interview workflowGate",
968
+ "todo-write-unknown-root-key": "todo_write root accepts only an ops array of operation entries",
969
+ "todo-write-unknown-op-entry-key":
970
+ "todo_write operation entries accept only op, list, task, phase, items, and text keys",
971
+ "todo-write-done-drop-requires-target": "todo_write done and drop entries require a task or phase target",
972
+ "todo-write-unknown-init-entry-key": "todo_write init list entries accept only phase and items keys",
968
973
  };
969
974
 
970
975
  /**
package/src/utils.ts CHANGED
@@ -199,6 +199,62 @@ function asLowerString(value: unknown): string | undefined {
199
199
  return typeof value === "string" ? value.toLowerCase() : undefined;
200
200
  }
201
201
 
202
+ /**
203
+ * Shape-tolerant classifier for the DeepSeek-family reasoning-content replay
204
+ * rejection: "The `reasoning_content` in the thinking mode must be passed back
205
+ * to the API." DeepSeek V4 (and reasoning-capable siblings reached through any
206
+ * OpenAI-compatible proxy) 400 every follow-up turn once a prior assistant turn
207
+ * carried reasoning the proxy stripped to an empty `encrypted_content` /
208
+ * `reasoning_content`. Resending the identical history re-triggers it, so naive
209
+ * session auto-retry just burns the budget — it needs the same bounded
210
+ * repair-and-resend contract as the `invalid_prompt` poisoned-history breaker.
211
+ *
212
+ * Accepts a raw provider error, an assistant message, or any object carrying an
213
+ * `errorMessage` field (the agent-loop circuit breaker keys on this).
214
+ */
215
+ export function isReasoningContentReplayError(input: unknown): boolean {
216
+ if (!input) return false;
217
+ const message =
218
+ typeof input === "string"
219
+ ? input
220
+ : input && typeof input === "object"
221
+ ? ((input as { errorMessage?: unknown; message?: unknown }).errorMessage ??
222
+ (input as { message?: unknown }).message)
223
+ : undefined;
224
+ return typeof message === "string" && REASONING_CONTENT_REPLAY_MESSAGE_RE.test(message);
225
+ }
226
+
227
+ const REASONING_CONTENT_REPLAY_MESSAGE_RE =
228
+ /reasoning_content[\s\S]*must be passed back to the API|reasoning content[\s\S]*must be passed back to the API/i;
229
+
230
+ /**
231
+ * Remove Responses-API `reasoning` items whose `encrypted_content` is missing or
232
+ * empty from an outgoing history payload. DeepSeek rejects replay of reasoning
233
+ * whose encrypted blob a proxy stripped to `""`; dropping those items lets the
234
+ * model re-reason on the next turn instead of re-triggering a deterministic 400.
235
+ * Non-reasoning items (text, function_call, function_call_output, ...) are kept
236
+ * verbatim so tool-use pairing and message order are preserved. Returns whether
237
+ * any item was actually removed — the circuit breaker uses this to decide
238
+ * between a single repaired resend (removed) and immediate fail-fast (unchanged).
239
+ */
240
+ export function stripUnusableReasoningItems(items: Array<Record<string, unknown>>): {
241
+ result: Array<Record<string, unknown>>;
242
+ removed: number;
243
+ } {
244
+ let removed = 0;
245
+ const result: Array<Record<string, unknown>> = [];
246
+ for (const item of items) {
247
+ if (item?.type === "reasoning") {
248
+ const encrypted = item.encrypted_content;
249
+ if (encrypted === undefined || encrypted === null || encrypted === "") {
250
+ removed++;
251
+ continue;
252
+ }
253
+ }
254
+ result.push(item);
255
+ }
256
+ return { result, removed };
257
+ }
202
258
  /**
203
259
  * Neutralize leaked reserved control tokens across every string in an outgoing
204
260
  * Responses `input` array. This is the request-boundary complement to the