@dbx-tools/shared-model 0.3.26 → 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.26"
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.26",
22
+ "version": "0.3.28",
23
23
  "types": "index.ts",
24
24
  "type": "module",
25
25
  "exports": {
@@ -29,6 +29,9 @@
29
29
  * - {@link sanitizeResponsesTools} keeps only `function` tools on a Responses
30
30
  * request body (for `/open-responses` / Anthropic, which reject Codex
31
31
  * built-ins like `web_search`).
32
+ * - {@link sanitizeOpenResponsesInput} rewrites `output_*` content part types
33
+ * in `input` to `input_*`, and drops Claude thinking / reasoning blocks
34
+ * that Open Responses rejects on replay (`redacted_thinking`, …).
32
35
  *
33
36
  * Only the surface real clients exercise is translated; unknown fields are
34
37
  * ignored rather than rejected, so a newer client degrades instead of breaking.
@@ -643,3 +646,95 @@ export function sanitizeResponsesTools(
643
646
  }
644
647
  return next;
645
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
+
653
+ /**
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.
669
+ *
670
+ * Returns a shallow copy when anything changes; otherwise the input body.
671
+ */
672
+ export function sanitizeOpenResponsesInput(
673
+ body: Record<string, unknown>,
674
+ ): Record<string, unknown> {
675
+ if (!Array.isArray(body.input) || body.input.length === 0) return body;
676
+
677
+ let changed = false;
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
+ }
685
+ const item = raw as Record<string, unknown>;
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
+ }
697
+
698
+ let partChanged = false;
699
+ const content: unknown[] = [];
700
+ for (const part of item.content) {
701
+ if (!part || typeof part !== "object") {
702
+ content.push(part);
703
+ continue;
704
+ }
705
+ const p = part as Record<string, unknown>;
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
+ }
722
+ changed = true;
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
+ }
727
+
728
+ return changed ? { ...body, input } : body;
729
+ }
730
+
731
+ /**
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.
735
+ */
736
+ export function sanitizeOpenResponsesRequest(
737
+ body: Record<string, unknown>,
738
+ ): Record<string, unknown> {
739
+ return sanitizeOpenResponsesInput(sanitizeResponsesTools(body));
740
+ }
@@ -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
+ });