@dbx-tools/shared-model 0.3.22 → 0.3.24
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/display.ts +1 -5
- package/src/openai-responses.ts +166 -3
- package/test/display.test.ts +45 -0
- package/test/openai-chat.test.ts +80 -0
- package/test/openai-responses.test.ts +242 -0
- package/test/tsconfig.json +14 -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.24"
|
|
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.24",
|
|
23
23
|
"types": "index.ts",
|
|
24
24
|
"type": "module",
|
|
25
25
|
"exports": {
|
package/src/display.ts
CHANGED
|
@@ -103,11 +103,7 @@ function renderSegment(segment: string): string {
|
|
|
103
103
|
const token = tokens[i]!;
|
|
104
104
|
const lower = token.toLowerCase();
|
|
105
105
|
// Glue a size unit onto the preceding number: `120` + `b` -> `120B`.
|
|
106
|
-
if (
|
|
107
|
-
SIZE_UNITS.has(lower) &&
|
|
108
|
-
out.length > 0 &&
|
|
109
|
-
/^\d+$/.test(out[out.length - 1]!)
|
|
110
|
-
) {
|
|
106
|
+
if (SIZE_UNITS.has(lower) && out.length > 0 && /^\d+$/.test(out[out.length - 1]!)) {
|
|
111
107
|
out[out.length - 1] = `${out[out.length - 1]}${lower.toUpperCase()}`;
|
|
112
108
|
continue;
|
|
113
109
|
}
|
package/src/openai-responses.ts
CHANGED
|
@@ -2,16 +2,22 @@
|
|
|
2
2
|
* OpenAI Responses API <-> Chat Completions translation.
|
|
3
3
|
*
|
|
4
4
|
* Some clients speak only the Responses API (`POST /v1/responses`) - the Codex
|
|
5
|
-
* CLI is the motivating one
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* CLI is the motivating one. Databricks now also exposes native Responses
|
|
6
|
+
* surfaces (`/serving-endpoints/responses` and `/serving-endpoints/open-responses`),
|
|
7
|
+
* while most endpoints still speak Chat Completions at `/invocations`. This
|
|
8
|
+
* module bridges the two, in both directions:
|
|
8
9
|
*
|
|
9
10
|
* - {@link responsesToChat} lowers a Responses request body to a chat
|
|
10
11
|
* completions body (instructions -> system message, typed `input` items ->
|
|
11
12
|
* `messages`, `tools`/`tool_choice` carried through, function-call outputs
|
|
12
13
|
* folded back into the transcript).
|
|
14
|
+
* - {@link chatToResponsesRequest} raises a chat-completions request into a
|
|
15
|
+
* Responses request (the inverse of {@link responsesToChat}), for routing a
|
|
16
|
+
* chat client at a Responses-only model like Codex.
|
|
13
17
|
* - {@link chatToResponse} lifts a non-streaming chat completion back into a
|
|
14
18
|
* Responses `response` object.
|
|
19
|
+
* - {@link responseToChatCompletion} lifts a native Responses `response`
|
|
20
|
+
* back into a chat-completions body (the inverse of {@link chatToResponse}).
|
|
15
21
|
* - {@link createResponsesStreamTranslator} lifts a streaming chat completion
|
|
16
22
|
* (an OpenAI SSE `chat.completion.chunk` stream) into the Responses SSE
|
|
17
23
|
* event stream those clients consume (`response.created`,
|
|
@@ -139,6 +145,110 @@ export function responsesToChat(body: Record<string, unknown>): {
|
|
|
139
145
|
return { chat, stream: body.stream === true };
|
|
140
146
|
}
|
|
141
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Raise a chat-completions request into a Responses request body. Inverse of
|
|
150
|
+
* {@link responsesToChat}: used when a chat client hits a Responses-only
|
|
151
|
+
* Databricks model (e.g. Codex) so the proxy can POST to
|
|
152
|
+
* `/serving-endpoints/responses` without the client knowing.
|
|
153
|
+
*/
|
|
154
|
+
export function chatToResponsesRequest(body: Record<string, unknown>): {
|
|
155
|
+
responses: Record<string, unknown>;
|
|
156
|
+
stream: boolean;
|
|
157
|
+
} {
|
|
158
|
+
const messages = Array.isArray(body.messages) ? body.messages : [];
|
|
159
|
+
const input: unknown[] = [];
|
|
160
|
+
let instructions: string | undefined;
|
|
161
|
+
|
|
162
|
+
for (const raw of messages) {
|
|
163
|
+
if (!raw || typeof raw !== "object") continue;
|
|
164
|
+
const message = raw as Record<string, unknown>;
|
|
165
|
+
const role = typeof message.role === "string" ? message.role : "user";
|
|
166
|
+
|
|
167
|
+
// Leading system / developer messages become top-level `instructions`.
|
|
168
|
+
if ((role === "system" || role === "developer") && instructions === undefined && input.length === 0) {
|
|
169
|
+
const text = chatContentToText(message.content);
|
|
170
|
+
if (text) instructions = text;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (role === "tool") {
|
|
175
|
+
input.push({
|
|
176
|
+
type: "function_call_output",
|
|
177
|
+
call_id: String(message.tool_call_id ?? ""),
|
|
178
|
+
output: chatContentToText(message.content),
|
|
179
|
+
});
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (role === "assistant" && Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
|
|
184
|
+
for (const rawCall of message.tool_calls) {
|
|
185
|
+
if (!rawCall || typeof rawCall !== "object") continue;
|
|
186
|
+
const call = rawCall as Record<string, unknown>;
|
|
187
|
+
const fn = (call.function ?? {}) as Record<string, unknown>;
|
|
188
|
+
input.push({
|
|
189
|
+
type: "function_call",
|
|
190
|
+
call_id: String(call.id ?? ""),
|
|
191
|
+
name: String(fn.name ?? ""),
|
|
192
|
+
arguments: typeof fn.arguments === "string" ? fn.arguments : "{}",
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const text = chatContentToText(message.content);
|
|
196
|
+
if (text) {
|
|
197
|
+
input.push({ type: "message", role: "assistant", content: text });
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
input.push({
|
|
203
|
+
type: "message",
|
|
204
|
+
role,
|
|
205
|
+
content: chatContentToText(message.content),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const responses: Record<string, unknown> = {
|
|
210
|
+
model: body.model,
|
|
211
|
+
input,
|
|
212
|
+
stream: body.stream === true,
|
|
213
|
+
};
|
|
214
|
+
if (instructions) responses.instructions = instructions;
|
|
215
|
+
|
|
216
|
+
// Chat tools are nested (`function: { name, … }`); Responses wants them flat.
|
|
217
|
+
if (Array.isArray(body.tools) && body.tools.length > 0) {
|
|
218
|
+
const tools = body.tools
|
|
219
|
+
.map((t) => {
|
|
220
|
+
if (!t || typeof t !== "object") return undefined;
|
|
221
|
+
const tool = t as Record<string, unknown>;
|
|
222
|
+
if (tool.type !== "function") return undefined;
|
|
223
|
+
if (tool.function && typeof tool.function === "object") {
|
|
224
|
+
const fn = tool.function as Record<string, unknown>;
|
|
225
|
+
return {
|
|
226
|
+
type: "function",
|
|
227
|
+
name: fn.name,
|
|
228
|
+
...(fn.description ? { description: fn.description } : {}),
|
|
229
|
+
...(fn.parameters ? { parameters: fn.parameters } : {}),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
if (typeof tool.name === "string") return tool;
|
|
233
|
+
return undefined;
|
|
234
|
+
})
|
|
235
|
+
.filter((t): t is Record<string, unknown> => t !== undefined);
|
|
236
|
+
if (tools.length > 0) {
|
|
237
|
+
responses.tools = tools;
|
|
238
|
+
if (body.tool_choice !== undefined) responses.tool_choice = body.tool_choice;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Prefer Responses' max_output_tokens naming; fall back from chat max_tokens.
|
|
243
|
+
if (typeof body.max_output_tokens === "number") {
|
|
244
|
+
responses.max_output_tokens = body.max_output_tokens;
|
|
245
|
+
} else if (typeof body.max_tokens === "number") {
|
|
246
|
+
responses.max_output_tokens = body.max_tokens;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return { responses, stream: body.stream === true };
|
|
250
|
+
}
|
|
251
|
+
|
|
142
252
|
/** Lift a non-streaming chat completion JSON into a Responses `response` object. */
|
|
143
253
|
export function chatToResponse(chat: Record<string, unknown>, model: string): unknown {
|
|
144
254
|
const choices = Array.isArray(chat.choices) ? chat.choices : [];
|
|
@@ -184,6 +294,59 @@ export function chatToResponse(chat: Record<string, unknown>, model: string): un
|
|
|
184
294
|
};
|
|
185
295
|
}
|
|
186
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Lift a native Responses `response` object into a chat-completions body.
|
|
299
|
+
* Inverse of {@link chatToResponse}: used after a Responses-only upstream call
|
|
300
|
+
* when the client spoke Chat Completions.
|
|
301
|
+
*/
|
|
302
|
+
export function responseToChatCompletion(
|
|
303
|
+
response: Record<string, unknown>,
|
|
304
|
+
model: string,
|
|
305
|
+
): Record<string, unknown> {
|
|
306
|
+
const { text } = readResponsesOutput(response);
|
|
307
|
+
const toolCalls: unknown[] = [];
|
|
308
|
+
const output = Array.isArray(response.output) ? response.output : [];
|
|
309
|
+
for (const raw of output) {
|
|
310
|
+
if (!raw || typeof raw !== "object") continue;
|
|
311
|
+
const item = raw as Record<string, unknown>;
|
|
312
|
+
if (item.type !== "function_call") continue;
|
|
313
|
+
toolCalls.push({
|
|
314
|
+
id: String(item.call_id ?? item.id ?? ""),
|
|
315
|
+
type: "function",
|
|
316
|
+
function: {
|
|
317
|
+
name: String(item.name ?? ""),
|
|
318
|
+
arguments: typeof item.arguments === "string" ? item.arguments : "{}",
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const message: Record<string, unknown> = {
|
|
324
|
+
role: "assistant",
|
|
325
|
+
content: text || null,
|
|
326
|
+
};
|
|
327
|
+
if (toolCalls.length > 0) message.tool_calls = toolCalls;
|
|
328
|
+
|
|
329
|
+
const usage = (response.usage ?? {}) as Record<string, unknown>;
|
|
330
|
+
return {
|
|
331
|
+
id: String(response.id ?? "chatcmpl"),
|
|
332
|
+
object: "chat.completion",
|
|
333
|
+
created: typeof response.created_at === "number" ? response.created_at : 0,
|
|
334
|
+
model,
|
|
335
|
+
choices: [
|
|
336
|
+
{
|
|
337
|
+
index: 0,
|
|
338
|
+
message,
|
|
339
|
+
finish_reason: toolCalls.length > 0 ? "tool_calls" : "stop",
|
|
340
|
+
},
|
|
341
|
+
],
|
|
342
|
+
usage: {
|
|
343
|
+
prompt_tokens: Number(usage.input_tokens ?? 0),
|
|
344
|
+
completion_tokens: Number(usage.output_tokens ?? 0),
|
|
345
|
+
total_tokens: Number(usage.total_tokens ?? 0),
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
187
350
|
/** One Server-Sent Event, ready to write to the client. */
|
|
188
351
|
function sse(event: string, data: unknown): string {
|
|
189
352
|
return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { toModelDisplayName } from "../src/display";
|
|
4
|
+
|
|
5
|
+
describe("toModelDisplayName", () => {
|
|
6
|
+
it("strips a leading databricks prefix and dots the version", () => {
|
|
7
|
+
assert.equal(toModelDisplayName("databricks-claude-sonnet-4-6"), "Claude Sonnet 4.6");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("strips the system.ai namespace and uppercases acronyms", () => {
|
|
11
|
+
assert.equal(toModelDisplayName("system.ai.bge_large_en"), "BGE Large En");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("uppercases GPT/OSS and glues the size unit", () => {
|
|
15
|
+
assert.equal(toModelDisplayName("databricks-gpt-oss-120b"), "GPT OSS 120B");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("restores AI casing", () => {
|
|
19
|
+
assert.equal(toModelDisplayName("databricks-ai-router"), "AI Router");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("joins multi-part version runs with dots", () => {
|
|
23
|
+
assert.equal(
|
|
24
|
+
toModelDisplayName("databricks-meta-llama-3-3-70b-instruct"),
|
|
25
|
+
"Meta Llama 3.3 70B Instruct",
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("prefers a Databricks-provided name verbatim", () => {
|
|
30
|
+
assert.equal(
|
|
31
|
+
toModelDisplayName("databricks-claude-sonnet-4-6", "Claude Sonnet 4.6 (Preview)"),
|
|
32
|
+
"Claude Sonnet 4.6 (Preview)",
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("ignores a blank provided name and falls back to the derived label", () => {
|
|
37
|
+
assert.equal(toModelDisplayName("databricks-gpt-5-5", " "), "GPT 5.5");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("title-cases a bare vendor token when nothing else remains", () => {
|
|
41
|
+
// Only leading vendor prefixes are stripped, and never the last
|
|
42
|
+
// remaining token, so a name that is *only* a vendor word stays.
|
|
43
|
+
assert.equal(toModelDisplayName("databricks"), "Databricks");
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
UNSUPPORTED_CHAT_FIELDS,
|
|
5
|
+
chatContentToText,
|
|
6
|
+
stripUnsupportedChatFields,
|
|
7
|
+
} from "../src/openai-chat";
|
|
8
|
+
|
|
9
|
+
describe("chatContentToText", () => {
|
|
10
|
+
it("passes a string through and flattens typed parts", () => {
|
|
11
|
+
assert.equal(chatContentToText("hello"), "hello");
|
|
12
|
+
assert.equal(
|
|
13
|
+
chatContentToText([{ type: "input_text", text: "a" }, { type: "output_text", text: "b" }]),
|
|
14
|
+
"ab",
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("honors a separator and a type filter", () => {
|
|
19
|
+
const content = [
|
|
20
|
+
{ type: "text", text: "one" },
|
|
21
|
+
{ type: "thinking", text: "hidden" },
|
|
22
|
+
{ type: "text", text: "two" },
|
|
23
|
+
];
|
|
24
|
+
assert.equal(chatContentToText(content, { separator: "\n\n", types: ["text"] }), "one\n\ntwo");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("yields empty string for anything it can't read", () => {
|
|
28
|
+
assert.equal(chatContentToText(null), "");
|
|
29
|
+
assert.equal(chatContentToText(undefined), "");
|
|
30
|
+
assert.equal(chatContentToText([{ type: "image_url" }]), "");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("stripUnsupportedChatFields", () => {
|
|
35
|
+
it("drops parallel_tool_calls, the field Databricks rejects outright", () => {
|
|
36
|
+
const body: Record<string, unknown> = {
|
|
37
|
+
model: "databricks-claude-opus-4-8",
|
|
38
|
+
messages: [],
|
|
39
|
+
parallel_tool_calls: false,
|
|
40
|
+
};
|
|
41
|
+
assert.deepEqual(stripUnsupportedChatFields(body), ["parallel_tool_calls"]);
|
|
42
|
+
assert.deepEqual(body, { model: "databricks-claude-opus-4-8", messages: [] });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("drops a field present with a falsy or null value", () => {
|
|
46
|
+
const body: Record<string, unknown> = { store: false, metadata: null };
|
|
47
|
+
assert.deepEqual(stripUnsupportedChatFields(body).sort(), ["metadata", "store"]);
|
|
48
|
+
assert.deepEqual(body, {});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("leaves a supported body untouched and reports nothing dropped", () => {
|
|
52
|
+
const body: Record<string, unknown> = {
|
|
53
|
+
model: "m",
|
|
54
|
+
messages: [{ role: "user", content: "hi" }],
|
|
55
|
+
stream: true,
|
|
56
|
+
tools: [],
|
|
57
|
+
tool_choice: "auto",
|
|
58
|
+
temperature: 0.2,
|
|
59
|
+
max_tokens: 64,
|
|
60
|
+
};
|
|
61
|
+
const before = structuredClone(body);
|
|
62
|
+
assert.deepEqual(stripUnsupportedChatFields(body), []);
|
|
63
|
+
assert.deepEqual(body, before);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("drops caller-supplied extra fields alongside the built-in list", () => {
|
|
67
|
+
const body: Record<string, unknown> = { store: true, future_field: 1, keep: 2 };
|
|
68
|
+
assert.deepEqual(stripUnsupportedChatFields(body, ["future_field"]), [
|
|
69
|
+
"store",
|
|
70
|
+
"future_field",
|
|
71
|
+
]);
|
|
72
|
+
assert.deepEqual(body, { keep: 2 });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("covers every name it advertises", () => {
|
|
76
|
+
const body = Object.fromEntries(UNSUPPORTED_CHAT_FIELDS.map((field) => [field, 1]));
|
|
77
|
+
assert.deepEqual(stripUnsupportedChatFields(body), [...UNSUPPORTED_CHAT_FIELDS]);
|
|
78
|
+
assert.deepEqual(body, {});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import type { ChatMessage } from "../src/openai-chat";
|
|
4
|
+
import {
|
|
5
|
+
chatToResponse,
|
|
6
|
+
createResponsesStreamTranslator,
|
|
7
|
+
readResponsesOutput,
|
|
8
|
+
responsesToChat,
|
|
9
|
+
} from "../src/openai-responses";
|
|
10
|
+
|
|
11
|
+
/** The `messages` a lowered Responses body produced, typed for assertions. */
|
|
12
|
+
function messagesOf(body: Record<string, unknown>): ChatMessage[] {
|
|
13
|
+
const { chat } = responsesToChat(body);
|
|
14
|
+
return chat.messages as ChatMessage[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Event names, in order, from a block of SSE text. */
|
|
18
|
+
function eventNames(sse: string): string[] {
|
|
19
|
+
return sse
|
|
20
|
+
.split("\n")
|
|
21
|
+
.filter((line) => line.startsWith("event: "))
|
|
22
|
+
.map((line) => line.slice("event: ".length));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Parsed `data:` payloads from a block of SSE text. */
|
|
26
|
+
function eventData(sse: string): Record<string, unknown>[] {
|
|
27
|
+
return sse
|
|
28
|
+
.split("\n")
|
|
29
|
+
.filter((line) => line.startsWith("data: "))
|
|
30
|
+
.map((line) => JSON.parse(line.slice("data: ".length)) as Record<string, unknown>);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("responsesToChat", () => {
|
|
34
|
+
it("lowers instructions and message items into a chat transcript", () => {
|
|
35
|
+
const messages = messagesOf({
|
|
36
|
+
instructions: "be terse",
|
|
37
|
+
input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "hi" }] }],
|
|
38
|
+
});
|
|
39
|
+
assert.deepEqual(messages, [
|
|
40
|
+
{ role: "system", content: "be terse" },
|
|
41
|
+
{ role: "user", content: "hi" },
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("maps the developer role to system (chat has no developer role)", () => {
|
|
46
|
+
const [first] = messagesOf({ input: [{ role: "developer", content: "policy" }] });
|
|
47
|
+
assert.equal(first?.role, "system");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("re-attaches a prior function_call to an assistant turn", () => {
|
|
51
|
+
const [first] = messagesOf({
|
|
52
|
+
input: [{ type: "function_call", call_id: "c1", name: "lookup", arguments: '{"q":1}' }],
|
|
53
|
+
});
|
|
54
|
+
assert.equal(first?.role, "assistant");
|
|
55
|
+
assert.equal(first?.content, null);
|
|
56
|
+
assert.deepEqual(first?.tool_calls, [
|
|
57
|
+
{ id: "c1", type: "function", function: { name: "lookup", arguments: '{"q":1}' } },
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("keys a function_call_output back to the call it answers", () => {
|
|
62
|
+
const [first] = messagesOf({
|
|
63
|
+
input: [{ type: "function_call_output", call_id: "c1", output: "42" }],
|
|
64
|
+
});
|
|
65
|
+
assert.deepEqual(first, { role: "tool", tool_call_id: "c1", content: "42" });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("nests flat Responses function tools and drops built-in tool types", () => {
|
|
69
|
+
const { chat } = responsesToChat({
|
|
70
|
+
input: [],
|
|
71
|
+
tools: [
|
|
72
|
+
{ type: "function", name: "lookup", description: "d", parameters: { type: "object" } },
|
|
73
|
+
{ type: "local_shell" },
|
|
74
|
+
{ type: "web_search" },
|
|
75
|
+
],
|
|
76
|
+
tool_choice: "auto",
|
|
77
|
+
});
|
|
78
|
+
assert.deepEqual(chat.tools, [
|
|
79
|
+
{
|
|
80
|
+
type: "function",
|
|
81
|
+
function: { name: "lookup", description: "d", parameters: { type: "object" } },
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
assert.equal(chat.tool_choice, "auto");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("never forwards parallel_tool_calls (Databricks rejects unknown fields)", () => {
|
|
88
|
+
const { chat } = responsesToChat({
|
|
89
|
+
input: [],
|
|
90
|
+
parallel_tool_calls: true,
|
|
91
|
+
tools: [{ type: "function", name: "lookup" }],
|
|
92
|
+
});
|
|
93
|
+
assert.ok(!("parallel_tool_calls" in chat));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("reports the streaming flag", () => {
|
|
97
|
+
assert.equal(responsesToChat({ input: [] }).stream, false);
|
|
98
|
+
assert.equal(responsesToChat({ input: [], stream: true }).stream, true);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("chatToResponse", () => {
|
|
103
|
+
it("lifts assistant text, tool calls, and usage into a response object", () => {
|
|
104
|
+
const response = chatToResponse(
|
|
105
|
+
{
|
|
106
|
+
id: "chatcmpl-1",
|
|
107
|
+
created: 1700000000,
|
|
108
|
+
choices: [
|
|
109
|
+
{
|
|
110
|
+
message: {
|
|
111
|
+
content: "done",
|
|
112
|
+
tool_calls: [{ id: "c1", function: { name: "lookup", arguments: '{"q":1}' } }],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
usage: { prompt_tokens: 3, completion_tokens: 4, total_tokens: 7 },
|
|
117
|
+
},
|
|
118
|
+
"databricks-claude-opus-4-8",
|
|
119
|
+
) as Record<string, unknown>;
|
|
120
|
+
|
|
121
|
+
assert.equal(response.id, "chatcmpl-1");
|
|
122
|
+
assert.equal(response.status, "completed");
|
|
123
|
+
assert.equal(response.model, "databricks-claude-opus-4-8");
|
|
124
|
+
assert.deepEqual(response.output, [
|
|
125
|
+
{ type: "message", role: "assistant", content: [{ type: "output_text", text: "done" }] },
|
|
126
|
+
{ type: "function_call", call_id: "c1", name: "lookup", arguments: '{"q":1}' },
|
|
127
|
+
]);
|
|
128
|
+
assert.deepEqual(response.usage, { input_tokens: 3, output_tokens: 4, total_tokens: 7 });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("omits the message item when the turn was tool calls only", () => {
|
|
132
|
+
const response = chatToResponse(
|
|
133
|
+
{ choices: [{ message: { content: null, tool_calls: [] } }] },
|
|
134
|
+
"m",
|
|
135
|
+
) as { output: unknown[] };
|
|
136
|
+
assert.deepEqual(response.output, []);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("createResponsesStreamTranslator", () => {
|
|
141
|
+
it("opens on the first chunk and closes the text run at finish", () => {
|
|
142
|
+
const translator = createResponsesStreamTranslator("m", "resp-1");
|
|
143
|
+
const first = translator.feed({ choices: [{ delta: { content: "he" } }] });
|
|
144
|
+
assert.deepEqual(eventNames(first), [
|
|
145
|
+
"response.created",
|
|
146
|
+
"response.output_item.added",
|
|
147
|
+
"response.output_text.delta",
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
const second = translator.feed({ choices: [{ delta: { content: "llo" } }] });
|
|
151
|
+
assert.deepEqual(eventNames(second), ["response.output_text.delta"]);
|
|
152
|
+
|
|
153
|
+
const done = translator.finish();
|
|
154
|
+
assert.deepEqual(eventNames(done), [
|
|
155
|
+
"response.output_text.done",
|
|
156
|
+
"response.output_item.done",
|
|
157
|
+
"response.completed",
|
|
158
|
+
]);
|
|
159
|
+
// The buffered text is what the client renders as the final message.
|
|
160
|
+
assert.equal(eventData(done)[0]?.text, "hello");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("assembles argument fragments into one function_call item", () => {
|
|
164
|
+
const translator = createResponsesStreamTranslator("m", "resp-1");
|
|
165
|
+
translator.feed({
|
|
166
|
+
choices: [
|
|
167
|
+
{ delta: { tool_calls: [{ index: 0, id: "c1", function: { name: "lookup" } }] } },
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
translator.feed({ choices: [{ delta: { tool_calls: [{ index: 0, function: { arguments: '{"q' } }] } }] });
|
|
171
|
+
translator.feed({ choices: [{ delta: { tool_calls: [{ index: 0, function: { arguments: '":1}' } }] } }] });
|
|
172
|
+
|
|
173
|
+
const done = translator.finish();
|
|
174
|
+
const [args] = eventData(done);
|
|
175
|
+
assert.equal(args?.arguments, '{"q":1}');
|
|
176
|
+
assert.ok(eventNames(done).includes("response.function_call_arguments.done"));
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("closes an open text run before opening a tool call so output stays ordered", () => {
|
|
180
|
+
const translator = createResponsesStreamTranslator("m", "resp-1");
|
|
181
|
+
translator.feed({ choices: [{ delta: { content: "thinking" } }] });
|
|
182
|
+
const withTool = translator.feed({
|
|
183
|
+
choices: [{ delta: { tool_calls: [{ index: 0, id: "c1", function: { name: "f" } }] } }],
|
|
184
|
+
});
|
|
185
|
+
assert.deepEqual(eventNames(withTool), [
|
|
186
|
+
"response.output_text.done",
|
|
187
|
+
"response.output_item.done",
|
|
188
|
+
"response.output_item.added",
|
|
189
|
+
]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("emits nothing but the lifecycle for an empty chunk", () => {
|
|
193
|
+
const translator = createResponsesStreamTranslator("m", "resp-1");
|
|
194
|
+
assert.deepEqual(eventNames(translator.feed({})), ["response.created"]);
|
|
195
|
+
assert.deepEqual(eventNames(translator.finish()), ["response.completed"]);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("readResponsesOutput", () => {
|
|
200
|
+
it("prefers the flattened output_text", () => {
|
|
201
|
+
const { text } = readResponsesOutput({
|
|
202
|
+
output_text: " the answer ",
|
|
203
|
+
output: [{ content: [{ text: "ignored" }] }],
|
|
204
|
+
});
|
|
205
|
+
assert.equal(text, "the answer");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("falls back to joining the output items", () => {
|
|
209
|
+
const { text } = readResponsesOutput({
|
|
210
|
+
output: [{ content: [{ text: "one" }, { text: "two" }] }],
|
|
211
|
+
});
|
|
212
|
+
assert.equal(text, "one\ntwo");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("collects url_citation annotations, deduplicated by url", () => {
|
|
216
|
+
const { citations } = readResponsesOutput({
|
|
217
|
+
output: [
|
|
218
|
+
{
|
|
219
|
+
content: [
|
|
220
|
+
{
|
|
221
|
+
text: "answer",
|
|
222
|
+
annotations: [
|
|
223
|
+
{ type: "url_citation", url: "https://a.example", title: "A" },
|
|
224
|
+
{ type: "url_citation", url: "https://a.example", title: "A again" },
|
|
225
|
+
{ type: "url_citation", url: "https://b.example" },
|
|
226
|
+
{ type: "file_citation" },
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
});
|
|
233
|
+
assert.deepEqual(citations, [
|
|
234
|
+
{ url: "https://a.example", title: "A" },
|
|
235
|
+
{ url: "https://b.example" },
|
|
236
|
+
]);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("returns empty results for a payload with no output", () => {
|
|
240
|
+
assert.deepEqual(readResponsesOutput({}), { text: "", citations: [] });
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// ~~ Generated by projen. To modify, edit .projenrc.js and run "pnpm exec projen".
|
|
2
|
+
{
|
|
3
|
+
"extends": "../tsconfig.json",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"noEmit": true,
|
|
6
|
+
"rootDir": ".."
|
|
7
|
+
},
|
|
8
|
+
"include": [
|
|
9
|
+
"**/*.ts"
|
|
10
|
+
],
|
|
11
|
+
"exclude": [
|
|
12
|
+
"node_modules"
|
|
13
|
+
]
|
|
14
|
+
}
|