@dbx-tools/shared-model 0.3.23 → 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/openai-responses.ts +166 -3
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/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`;
|