@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 +2 -2
- package/src/openai-responses.ts +95 -0
- package/test/openai-responses.test.ts +70 -0
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.
|
|
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.
|
|
22
|
+
"version": "0.3.28",
|
|
23
23
|
"types": "index.ts",
|
|
24
24
|
"type": "module",
|
|
25
25
|
"exports": {
|
package/src/openai-responses.ts
CHANGED
|
@@ -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
|
+
});
|