@dbx-tools/shared-model 0.3.27 → 0.3.28

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/package.json CHANGED
@@ -12,14 +12,14 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "zod": "^4.3.6",
15
- "@dbx-tools/shared-core": "0.3.27"
15
+ "@dbx-tools/shared-core": "0.3.28"
16
16
  },
17
17
  "main": "index.ts",
18
18
  "license": "UNLICENSED",
19
19
  "publishConfig": {
20
20
  "access": "public"
21
21
  },
22
- "version": "0.3.27",
22
+ "version": "0.3.28",
23
23
  "types": "index.ts",
24
24
  "type": "module",
25
25
  "exports": {
@@ -30,7 +30,8 @@
30
30
  * request body (for `/open-responses` / Anthropic, which reject Codex
31
31
  * built-ins like `web_search`).
32
32
  * - {@link sanitizeOpenResponsesInput} rewrites `output_*` content part types
33
- * in `input` to `input_*` (Open Responses rejects `output_text` in history).
33
+ * in `input` to `input_*`, and drops Claude thinking / reasoning blocks
34
+ * that Open Responses rejects on replay (`redacted_thinking`, …).
34
35
  *
35
36
  * Only the surface real clients exercise is translated; unknown fields are
36
37
  * ignored rather than rejected, so a newer client degrades instead of breaking.
@@ -646,17 +647,27 @@ export function sanitizeResponsesTools(
646
647
  return next;
647
648
  }
648
649
 
650
+ /** Content part / input item types Claude extended-thinking uses on the wire. */
651
+ const REASONING_TYPES = new Set(["reasoning", "thinking", "redacted_thinking"]);
652
+
649
653
  /**
650
- * Rewrite Responses `input` content parts so Open Responses will accept them.
654
+ * Rewrite Responses `input` so Open Responses will accept it.
655
+ *
656
+ * Two OpenAI / Codex shapes that Databricks Open Responses (Claude, Gemini, …)
657
+ * reject:
658
+ *
659
+ * 1. Prior assistant turns with `output_text` / `output_*` content parts —
660
+ * Open Responses only allows `input_text`, `input_image`, `input_file`,
661
+ * `input_audio`. Map every `output_<kind>` part to `input_<kind>`.
662
+ * 2. Replayed extended-thinking blocks (`thinking`, `redacted_thinking`,
663
+ * `reasoning` content parts, and top-level `reasoning` input items). Claude
664
+ * signs those; any client round-trip that mutates them (or a cross-provider
665
+ * replay) fails with e.g.
666
+ * "messages.N.content.M: Invalid `data` in `redacted_thinking` block"
667
+ * Strip them — the UI/client already showed the thinking; replay does not
668
+ * need the signed blob. Same policy as appkit-mastra's serving sanitize.
651
669
  *
652
- * OpenAI `/responses` (and Codex) replay prior assistant turns with
653
- * `output_text` / `output_*` content part types. Databricks Open Responses
654
- * (Claude, Gemini, …) only allows input-side types:
655
- * "Open Responses input content part type 'output_text' is not supported.
656
- * Supported: input_text, input_image, input_file, input_audio."
657
- * Map every `output_<kind>` part to `input_<kind>` (shallow copy; no mutation).
658
- * Call together with {@link sanitizeResponsesTools} before forwarding to
659
- * `/serving-endpoints/open-responses`.
670
+ * Returns a shallow copy when anything changes; otherwise the input body.
660
671
  */
661
672
  export function sanitizeOpenResponsesInput(
662
673
  body: Record<string, unknown>,
@@ -664,30 +675,63 @@ export function sanitizeOpenResponsesInput(
664
675
  if (!Array.isArray(body.input) || body.input.length === 0) return body;
665
676
 
666
677
  let changed = false;
667
- const input = body.input.map((raw) => {
668
- if (!raw || typeof raw !== "object") return raw;
678
+ const input: unknown[] = [];
679
+
680
+ for (const raw of body.input) {
681
+ if (!raw || typeof raw !== "object") {
682
+ input.push(raw);
683
+ continue;
684
+ }
669
685
  const item = raw as Record<string, unknown>;
670
- if (!Array.isArray(item.content)) return raw;
686
+
687
+ // Top-level Responses reasoning items (not message content parts).
688
+ if (typeof item.type === "string" && REASONING_TYPES.has(item.type)) {
689
+ changed = true;
690
+ continue;
691
+ }
692
+
693
+ if (!Array.isArray(item.content)) {
694
+ input.push(raw);
695
+ continue;
696
+ }
671
697
 
672
698
  let partChanged = false;
673
- const content = item.content.map((part) => {
674
- if (!part || typeof part !== "object") return part;
699
+ const content: unknown[] = [];
700
+ for (const part of item.content) {
701
+ if (!part || typeof part !== "object") {
702
+ content.push(part);
703
+ continue;
704
+ }
675
705
  const p = part as Record<string, unknown>;
676
- if (typeof p.type !== "string" || !p.type.startsWith("output_")) return part;
677
- partChanged = true;
678
- return { ...p, type: `input_${p.type.slice("output_".length)}` };
679
- });
680
- if (!partChanged) return raw;
706
+ if (typeof p.type === "string" && REASONING_TYPES.has(p.type)) {
707
+ partChanged = true;
708
+ continue;
709
+ }
710
+ if (typeof p.type === "string" && p.type.startsWith("output_")) {
711
+ partChanged = true;
712
+ content.push({ ...p, type: `input_${p.type.slice("output_".length)}` });
713
+ continue;
714
+ }
715
+ content.push(part);
716
+ }
717
+
718
+ if (!partChanged) {
719
+ input.push(raw);
720
+ continue;
721
+ }
681
722
  changed = true;
682
- return { ...item, content };
683
- });
723
+ // Drop assistant turns that only carried thinking (nothing left to replay).
724
+ if (content.length === 0 && item.role === "assistant") continue;
725
+ input.push({ ...item, content });
726
+ }
684
727
 
685
728
  return changed ? { ...body, input } : body;
686
729
  }
687
730
 
688
731
  /**
689
- * Full Open Responses request sanitizer: strip non-`function` tools and rewrite
690
- * `output_*` input content parts. Safe no-op when nothing needs changing.
732
+ * Full Open Responses request sanitizer: strip non-`function` tools, rewrite
733
+ * `output_*` content parts, and drop thinking / reasoning blocks. Safe no-op
734
+ * when nothing needs changing.
691
735
  */
692
736
  export function sanitizeOpenResponsesRequest(
693
737
  body: Record<string, unknown>,
@@ -6,6 +6,7 @@ import {
6
6
  createResponsesStreamTranslator,
7
7
  readResponsesOutput,
8
8
  responsesToChat,
9
+ sanitizeOpenResponsesRequest,
9
10
  } from "../src/openai-responses";
10
11
 
11
12
  /** The `messages` a lowered Responses body produced, typed for assertions. */
@@ -240,3 +241,72 @@ describe("readResponsesOutput", () => {
240
241
  assert.deepEqual(readResponsesOutput({}), { text: "", citations: [] });
241
242
  });
242
243
  });
244
+
245
+ describe("sanitizeOpenResponsesRequest", () => {
246
+ it("rewrites output_text content parts to input_text", () => {
247
+ const out = sanitizeOpenResponsesRequest({
248
+ input: [
249
+ {
250
+ type: "message",
251
+ role: "assistant",
252
+ content: [{ type: "output_text", text: "hi" }],
253
+ },
254
+ ],
255
+ });
256
+ assert.deepEqual(out.input, [
257
+ {
258
+ type: "message",
259
+ role: "assistant",
260
+ content: [{ type: "input_text", text: "hi" }],
261
+ },
262
+ ]);
263
+ });
264
+
265
+ it("strips redacted_thinking / thinking content parts and reasoning items", () => {
266
+ const out = sanitizeOpenResponsesRequest({
267
+ input: [
268
+ { type: "reasoning", summary: [{ type: "summary_text", text: "plan" }] },
269
+ {
270
+ type: "message",
271
+ role: "assistant",
272
+ content: [
273
+ { type: "redacted_thinking", data: "not-a-valid-blob" },
274
+ { type: "thinking", thinking: "secret" },
275
+ { type: "output_text", text: "visible" },
276
+ ],
277
+ },
278
+ ],
279
+ });
280
+ assert.deepEqual(out.input, [
281
+ {
282
+ type: "message",
283
+ role: "assistant",
284
+ content: [{ type: "input_text", text: "visible" }],
285
+ },
286
+ ]);
287
+ });
288
+
289
+ it("drops assistant turns that only carried thinking", () => {
290
+ const out = sanitizeOpenResponsesRequest({
291
+ input: [
292
+ {
293
+ type: "message",
294
+ role: "assistant",
295
+ content: [{ type: "redacted_thinking", data: "x" }],
296
+ },
297
+ { type: "message", role: "user", content: [{ type: "input_text", text: "next" }] },
298
+ ],
299
+ });
300
+ assert.deepEqual(out.input, [
301
+ { type: "message", role: "user", content: [{ type: "input_text", text: "next" }] },
302
+ ]);
303
+ });
304
+
305
+ it("strips non-function tools", () => {
306
+ const out = sanitizeOpenResponsesRequest({
307
+ input: [],
308
+ tools: [{ type: "web_search" }, { type: "function", name: "f", parameters: {} }],
309
+ });
310
+ assert.deepEqual(out.tools, [{ type: "function", name: "f", parameters: {} }]);
311
+ });
312
+ });