@dbx-tools/cli-model-proxy 0.3.21
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/README.md +143 -0
- package/bin/dbx-tools-model-proxy.mjs +11 -0
- package/bin/dbx-tools-model-proxy.ts +12 -0
- package/index.ts +11 -0
- package/package.json +59 -0
- package/src/backend.ts +146 -0
- package/src/cli.ts +200 -0
- package/src/defaults.ts +18 -0
- package/src/responses.ts +391 -0
- package/src/server.ts +416 -0
- package/tsconfig.json +43 -0
package/src/responses.ts
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Responses API <-> Chat Completions translation.
|
|
3
|
+
*
|
|
4
|
+
* The Codex CLI speaks only the Responses API (`POST /v1/responses`), but
|
|
5
|
+
* Databricks serving endpoints speak only Chat Completions (`messages`) at
|
|
6
|
+
* their `invocations` URL. This module bridges the two:
|
|
7
|
+
*
|
|
8
|
+
* - {@link responsesToChat} lowers a Responses request body to a chat
|
|
9
|
+
* completions body (instructions -> system message, typed `input` items ->
|
|
10
|
+
* `messages`, `tools`/`tool_choice` carried through, function-call outputs
|
|
11
|
+
* folded back into the transcript).
|
|
12
|
+
* - {@link chatToResponse} lifts a non-streaming chat completion back into a
|
|
13
|
+
* Responses `response` object.
|
|
14
|
+
* - {@link chatStreamToResponsesSse} lifts a streaming chat completion (an
|
|
15
|
+
* OpenAI SSE `chat.completion.chunk` stream) into the Responses SSE event
|
|
16
|
+
* stream Codex consumes (`response.created`, `response.output_text.delta`,
|
|
17
|
+
* function-call argument deltas, `response.completed`).
|
|
18
|
+
*
|
|
19
|
+
* Only the surface Codex actually exercises is translated; unknown fields are
|
|
20
|
+
* ignored rather than rejected, so a newer client degrades instead of breaking.
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** A minimal chat message as Databricks Model Serving expects it. */
|
|
26
|
+
interface ChatMessage {
|
|
27
|
+
role: string;
|
|
28
|
+
content?: string | null;
|
|
29
|
+
tool_calls?: ChatToolCall[];
|
|
30
|
+
tool_call_id?: string;
|
|
31
|
+
name?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ChatToolCall {
|
|
35
|
+
id: string;
|
|
36
|
+
type: "function";
|
|
37
|
+
function: { name: string; arguments: string };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Extract plain text from a Responses `content` value (string or typed parts). */
|
|
41
|
+
function contentToText(content: unknown): string {
|
|
42
|
+
if (typeof content === "string") return content;
|
|
43
|
+
if (!Array.isArray(content)) return "";
|
|
44
|
+
const parts: string[] = [];
|
|
45
|
+
for (const part of content) {
|
|
46
|
+
if (part && typeof part === "object") {
|
|
47
|
+
const p = part as Record<string, unknown>;
|
|
48
|
+
// input_text / output_text / text all carry their text on `.text`.
|
|
49
|
+
if (typeof p.text === "string") parts.push(p.text);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return parts.join("");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Lower a Responses request body to a chat-completions body. Returns the chat
|
|
57
|
+
* body plus whether the caller asked for streaming (the server needs it to pick
|
|
58
|
+
* the upstream `accept` header and the translation path).
|
|
59
|
+
*/
|
|
60
|
+
export function responsesToChat(body: Record<string, unknown>): {
|
|
61
|
+
chat: Record<string, unknown>;
|
|
62
|
+
stream: boolean;
|
|
63
|
+
} {
|
|
64
|
+
const messages: ChatMessage[] = [];
|
|
65
|
+
|
|
66
|
+
// `instructions` becomes the leading system message.
|
|
67
|
+
if (typeof body.instructions === "string" && body.instructions.length > 0) {
|
|
68
|
+
messages.push({ role: "system", content: body.instructions });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// `input` is an array of typed items: messages, function_call, and
|
|
72
|
+
// function_call_output. Fold each into the chat transcript.
|
|
73
|
+
const input = Array.isArray(body.input) ? body.input : [];
|
|
74
|
+
for (const raw of input) {
|
|
75
|
+
if (!raw || typeof raw !== "object") continue;
|
|
76
|
+
const item = raw as Record<string, unknown>;
|
|
77
|
+
const type = item.type;
|
|
78
|
+
|
|
79
|
+
if (type === "function_call") {
|
|
80
|
+
// A prior tool call the model made; re-attach it to an assistant turn.
|
|
81
|
+
messages.push({
|
|
82
|
+
role: "assistant",
|
|
83
|
+
content: null,
|
|
84
|
+
tool_calls: [
|
|
85
|
+
{
|
|
86
|
+
id: String(item.call_id ?? item.id ?? ""),
|
|
87
|
+
type: "function",
|
|
88
|
+
function: {
|
|
89
|
+
name: String(item.name ?? ""),
|
|
90
|
+
arguments: typeof item.arguments === "string" ? item.arguments : "{}",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (type === "function_call_output") {
|
|
99
|
+
// The tool's result, keyed back to the call it answers.
|
|
100
|
+
messages.push({
|
|
101
|
+
role: "tool",
|
|
102
|
+
tool_call_id: String(item.call_id ?? item.id ?? ""),
|
|
103
|
+
content: typeof item.output === "string" ? item.output : contentToText(item.output),
|
|
104
|
+
});
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Default: a message item with a role and typed content.
|
|
109
|
+
const role = typeof item.role === "string" ? item.role : "user";
|
|
110
|
+
// Chat has no "developer" role; treat it as system (its intent here).
|
|
111
|
+
const chatRole = role === "developer" ? "system" : role;
|
|
112
|
+
messages.push({ role: chatRole, content: contentToText(item.content) });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const chat: Record<string, unknown> = {
|
|
116
|
+
model: body.model,
|
|
117
|
+
messages,
|
|
118
|
+
stream: body.stream === true,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Carry through function-calling config when present. Responses function
|
|
122
|
+
// tools are FLAT (`{type:"function", name, description, parameters}`); chat
|
|
123
|
+
// wants them NESTED (`{type:"function", function:{name, …}}`). Codex also
|
|
124
|
+
// sends built-in tool types (local_shell, web_search, …) that Databricks chat
|
|
125
|
+
// rejects ("Missing 'function' in the tool specification"), so we translate
|
|
126
|
+
// only function tools and drop the rest.
|
|
127
|
+
if (Array.isArray(body.tools) && body.tools.length > 0) {
|
|
128
|
+
const chatTools = body.tools
|
|
129
|
+
.map((t) => {
|
|
130
|
+
const tool = t as Record<string, unknown>;
|
|
131
|
+
// Already chat-nested (`function` object present): pass through.
|
|
132
|
+
if (tool.type === "function" && tool.function && typeof tool.function === "object") {
|
|
133
|
+
return tool;
|
|
134
|
+
}
|
|
135
|
+
// Flat Responses function tool: nest it.
|
|
136
|
+
if (tool.type === "function" && typeof tool.name === "string") {
|
|
137
|
+
return {
|
|
138
|
+
type: "function",
|
|
139
|
+
function: {
|
|
140
|
+
name: tool.name,
|
|
141
|
+
...(tool.description ? { description: tool.description } : {}),
|
|
142
|
+
...(tool.parameters ? { parameters: tool.parameters } : {}),
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return undefined; // non-function built-in tool: unsupported upstream, drop
|
|
147
|
+
})
|
|
148
|
+
.filter((t): t is Record<string, unknown> => t !== undefined);
|
|
149
|
+
if (chatTools.length > 0) {
|
|
150
|
+
chat.tools = chatTools;
|
|
151
|
+
if (body.tool_choice !== undefined) chat.tool_choice = body.tool_choice;
|
|
152
|
+
if (body.parallel_tool_calls !== undefined) {
|
|
153
|
+
chat.parallel_tool_calls = body.parallel_tool_calls;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { chat, stream: body.stream === true };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Lift a non-streaming chat completion JSON into a Responses `response` object. */
|
|
162
|
+
export function chatToResponse(chat: Record<string, unknown>, model: string): unknown {
|
|
163
|
+
const choices = Array.isArray(chat.choices) ? chat.choices : [];
|
|
164
|
+
const first = (choices[0] ?? {}) as Record<string, unknown>;
|
|
165
|
+
const message = (first.message ?? {}) as Record<string, unknown>;
|
|
166
|
+
const output: unknown[] = [];
|
|
167
|
+
|
|
168
|
+
const text = typeof message.content === "string" ? message.content : "";
|
|
169
|
+
if (text) {
|
|
170
|
+
output.push({
|
|
171
|
+
type: "message",
|
|
172
|
+
role: "assistant",
|
|
173
|
+
content: [{ type: "output_text", text }],
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Surface any tool calls as Responses `function_call` output items.
|
|
178
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
179
|
+
for (const raw of toolCalls) {
|
|
180
|
+
const call = raw as Record<string, unknown>;
|
|
181
|
+
const fn = (call.function ?? {}) as Record<string, unknown>;
|
|
182
|
+
output.push({
|
|
183
|
+
type: "function_call",
|
|
184
|
+
call_id: String(call.id ?? ""),
|
|
185
|
+
name: String(fn.name ?? ""),
|
|
186
|
+
arguments: typeof fn.arguments === "string" ? fn.arguments : "{}",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const usage = (chat.usage ?? {}) as Record<string, unknown>;
|
|
191
|
+
return {
|
|
192
|
+
id: String(chat.id ?? "resp"),
|
|
193
|
+
object: "response",
|
|
194
|
+
created_at: typeof chat.created === "number" ? chat.created : 0,
|
|
195
|
+
model,
|
|
196
|
+
status: "completed",
|
|
197
|
+
output,
|
|
198
|
+
usage: {
|
|
199
|
+
input_tokens: Number(usage.prompt_tokens ?? 0),
|
|
200
|
+
output_tokens: Number(usage.completion_tokens ?? 0),
|
|
201
|
+
total_tokens: Number(usage.total_tokens ?? 0),
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** One Server-Sent Event, ready to write to the client. */
|
|
207
|
+
function sse(event: string, data: unknown): string {
|
|
208
|
+
return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Translate an upstream chat-completions SSE stream into the Responses SSE
|
|
213
|
+
* event stream Codex consumes.
|
|
214
|
+
*
|
|
215
|
+
* Upstream chunks are `chat.completion.chunk` objects whose `choices[0].delta`
|
|
216
|
+
* carries incremental `content` (assistant text) and/or `tool_calls` (function
|
|
217
|
+
* calls, streamed by `index` with partial `arguments`). We emit the Responses
|
|
218
|
+
* lifecycle around them:
|
|
219
|
+
*
|
|
220
|
+
* response.created
|
|
221
|
+
* -> per text run: output_item.added → output_text.delta* → output_item.done
|
|
222
|
+
* -> per tool call: output_item.added → function_call_arguments.delta*
|
|
223
|
+
* → function_call_arguments.done → output_item.done
|
|
224
|
+
* response.completed (with the assembled final `response` object)
|
|
225
|
+
*
|
|
226
|
+
* `feed(chunk)` returns the SSE bytes to forward for that upstream chunk;
|
|
227
|
+
* `finish()` returns the closing `response.completed` event. The generator is
|
|
228
|
+
* intentionally tolerant: malformed/keepalive lines yield nothing.
|
|
229
|
+
*/
|
|
230
|
+
export function createResponsesStreamTranslator(model: string, responseId: string) {
|
|
231
|
+
let started = false;
|
|
232
|
+
let outputIndex = 0;
|
|
233
|
+
|
|
234
|
+
// Text run state: whether a message item is currently open, and its buffer.
|
|
235
|
+
let textOpen = false;
|
|
236
|
+
let textBuffer = "";
|
|
237
|
+
let textItemId = "";
|
|
238
|
+
|
|
239
|
+
// Tool-call state, keyed by the upstream `tool_calls[].index`.
|
|
240
|
+
interface ToolState {
|
|
241
|
+
outputIndex: number;
|
|
242
|
+
callId: string;
|
|
243
|
+
name: string;
|
|
244
|
+
args: string;
|
|
245
|
+
}
|
|
246
|
+
const tools = new Map<number, ToolState>();
|
|
247
|
+
|
|
248
|
+
const created = () =>
|
|
249
|
+
sse("response.created", {
|
|
250
|
+
type: "response.created",
|
|
251
|
+
response: { id: responseId, object: "response", status: "in_progress", model, output: [] },
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
function openText(): string {
|
|
255
|
+
textOpen = true;
|
|
256
|
+
textItemId = `${responseId}-msg-${outputIndex}`;
|
|
257
|
+
const ev = sse("response.output_item.added", {
|
|
258
|
+
type: "response.output_item.added",
|
|
259
|
+
output_index: outputIndex,
|
|
260
|
+
item: { id: textItemId, type: "message", role: "assistant", content: [] },
|
|
261
|
+
});
|
|
262
|
+
return ev;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function closeText(): string {
|
|
266
|
+
if (!textOpen) return "";
|
|
267
|
+
const item = {
|
|
268
|
+
id: textItemId,
|
|
269
|
+
type: "message",
|
|
270
|
+
role: "assistant",
|
|
271
|
+
content: [{ type: "output_text", text: textBuffer }],
|
|
272
|
+
};
|
|
273
|
+
const ev =
|
|
274
|
+
sse("response.output_text.done", {
|
|
275
|
+
type: "response.output_text.done",
|
|
276
|
+
item_id: textItemId,
|
|
277
|
+
output_index: outputIndex,
|
|
278
|
+
content_index: 0,
|
|
279
|
+
text: textBuffer,
|
|
280
|
+
}) +
|
|
281
|
+
sse("response.output_item.done", {
|
|
282
|
+
type: "response.output_item.done",
|
|
283
|
+
output_index: outputIndex,
|
|
284
|
+
item,
|
|
285
|
+
});
|
|
286
|
+
textOpen = false;
|
|
287
|
+
textBuffer = "";
|
|
288
|
+
outputIndex += 1;
|
|
289
|
+
return ev;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function feed(chunk: Record<string, unknown>): string {
|
|
293
|
+
let out = "";
|
|
294
|
+
if (!started) {
|
|
295
|
+
started = true;
|
|
296
|
+
out += created();
|
|
297
|
+
}
|
|
298
|
+
const choices = Array.isArray(chunk.choices) ? chunk.choices : [];
|
|
299
|
+
const choice = (choices[0] ?? {}) as Record<string, unknown>;
|
|
300
|
+
const delta = (choice.delta ?? {}) as Record<string, unknown>;
|
|
301
|
+
|
|
302
|
+
// Assistant text deltas.
|
|
303
|
+
if (typeof delta.content === "string" && delta.content.length > 0) {
|
|
304
|
+
if (!textOpen) out += openText();
|
|
305
|
+
textBuffer += delta.content;
|
|
306
|
+
out += sse("response.output_text.delta", {
|
|
307
|
+
type: "response.output_text.delta",
|
|
308
|
+
item_id: textItemId,
|
|
309
|
+
output_index: outputIndex,
|
|
310
|
+
content_index: 0,
|
|
311
|
+
delta: delta.content,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Tool-call deltas: each is keyed by `index`; `arguments` arrives in pieces.
|
|
316
|
+
const toolDeltas = Array.isArray(delta.tool_calls) ? delta.tool_calls : [];
|
|
317
|
+
for (const rawTc of toolDeltas) {
|
|
318
|
+
const tc = rawTc as Record<string, unknown>;
|
|
319
|
+
const idx = Number(tc.index ?? 0);
|
|
320
|
+
const fn = (tc.function ?? {}) as Record<string, unknown>;
|
|
321
|
+
let state = tools.get(idx);
|
|
322
|
+
if (!state) {
|
|
323
|
+
// First fragment for this call: a text run (if open) must close first so
|
|
324
|
+
// output ordering stays monotonic, then we open a function_call item.
|
|
325
|
+
out += closeText();
|
|
326
|
+
state = {
|
|
327
|
+
outputIndex,
|
|
328
|
+
callId: String(tc.id ?? `${responseId}-call-${idx}`),
|
|
329
|
+
name: String(fn.name ?? ""),
|
|
330
|
+
args: "",
|
|
331
|
+
};
|
|
332
|
+
tools.set(idx, state);
|
|
333
|
+
out += sse("response.output_item.added", {
|
|
334
|
+
type: "response.output_item.added",
|
|
335
|
+
output_index: state.outputIndex,
|
|
336
|
+
item: {
|
|
337
|
+
id: state.callId,
|
|
338
|
+
type: "function_call",
|
|
339
|
+
call_id: state.callId,
|
|
340
|
+
name: state.name,
|
|
341
|
+
arguments: "",
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
outputIndex += 1;
|
|
345
|
+
}
|
|
346
|
+
if (typeof fn.name === "string" && fn.name && !state.name) state.name = fn.name;
|
|
347
|
+
if (typeof fn.arguments === "string" && fn.arguments.length > 0) {
|
|
348
|
+
state.args += fn.arguments;
|
|
349
|
+
out += sse("response.function_call_arguments.delta", {
|
|
350
|
+
type: "response.function_call_arguments.delta",
|
|
351
|
+
item_id: state.callId,
|
|
352
|
+
output_index: state.outputIndex,
|
|
353
|
+
delta: fn.arguments,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return out;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function finish(): string {
|
|
361
|
+
let out = closeText();
|
|
362
|
+
// Close any open tool calls, emitting their assembled arguments.
|
|
363
|
+
for (const state of tools.values()) {
|
|
364
|
+
out += sse("response.function_call_arguments.done", {
|
|
365
|
+
type: "response.function_call_arguments.done",
|
|
366
|
+
item_id: state.callId,
|
|
367
|
+
output_index: state.outputIndex,
|
|
368
|
+
arguments: state.args,
|
|
369
|
+
});
|
|
370
|
+
out += sse("response.output_item.done", {
|
|
371
|
+
type: "response.output_item.done",
|
|
372
|
+
output_index: state.outputIndex,
|
|
373
|
+
item: {
|
|
374
|
+
id: state.callId,
|
|
375
|
+
type: "function_call",
|
|
376
|
+
call_id: state.callId,
|
|
377
|
+
name: state.name,
|
|
378
|
+
arguments: state.args,
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
out += sse("response.completed", {
|
|
383
|
+
type: "response.completed",
|
|
384
|
+
response: { id: responseId, object: "response", status: "completed", model },
|
|
385
|
+
});
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return { feed, finish };
|
|
390
|
+
}
|
|
391
|
+
|