@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 +2 -2
- package/src/openai-responses.ts +68 -24
- 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
|
@@ -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_
|
|
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`
|
|
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
|
-
*
|
|
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 =
|
|
668
|
-
|
|
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
|
-
|
|
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 =
|
|
674
|
-
|
|
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
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
|
|
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
|
|
690
|
-
* `output_*`
|
|
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
|
+
});
|