@dbx-tools/cli-model-proxy 0.3.24 → 0.3.26

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.
Files changed (3) hide show
  1. package/package.json +4 -4
  2. package/src/cli.ts +24 -12
  3. package/src/server.ts +20 -4
package/package.json CHANGED
@@ -18,16 +18,16 @@
18
18
  "@databricks/sdk-experimental": "^0.17.0",
19
19
  "commander": "^15.0.0",
20
20
  "tsx": "^4.23.0",
21
- "@dbx-tools/model": "0.3.24",
22
- "@dbx-tools/shared-model": "0.3.24",
23
- "@dbx-tools/shared-core": "0.3.24"
21
+ "@dbx-tools/model": "0.3.26",
22
+ "@dbx-tools/shared-core": "0.3.26",
23
+ "@dbx-tools/shared-model": "0.3.26"
24
24
  },
25
25
  "main": "index.ts",
26
26
  "license": "UNLICENSED",
27
27
  "publishConfig": {
28
28
  "access": "public"
29
29
  },
30
- "version": "0.3.24",
30
+ "version": "0.3.26",
31
31
  "types": "index.ts",
32
32
  "type": "module",
33
33
  "exports": {
package/src/cli.ts CHANGED
@@ -20,7 +20,7 @@
20
20
 
21
21
  import { spawn } from "node:child_process";
22
22
  import type { Server } from "node:http";
23
- import { classify } from "@dbx-tools/shared-model";
23
+ import { classify, type ServingEndpointSummary } from "@dbx-tools/shared-model";
24
24
  import { Command, CommanderError } from "commander";
25
25
 
26
26
  import { DatabricksBackend, type BackendOptions } from "./backend";
@@ -157,15 +157,7 @@ export function buildProgram(): Command {
157
157
  const opts = globalOpts<CommonOpts & { chat?: boolean }>(command);
158
158
  const backend = await DatabricksBackend.create(backendOptions(opts));
159
159
  const endpoints = await backend.models();
160
- // Enrich each endpoint with a derived `capabilities` object so consumers
161
- // filter on capability rather than re-deriving it from raw `task`/`class`
162
- // strings. `chat` = OpenAI chat/completions + Responses (what an agent
163
- // like Codex needs); `embedding` = vector endpoints; `tools` = whether the
164
- // endpoint supports function/tool calls (all chat endpoints here do).
165
- const enriched = endpoints.map((endpoint) => ({
166
- ...endpoint,
167
- capabilities: classify.endpointCapabilities(endpoint),
168
- }));
160
+ const enriched = endpoints.map(enrichEndpoint);
169
161
  const out = opts.chat ? enriched.filter((e) => e.capabilities.chat) : enriched;
170
162
  process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
171
163
  });
@@ -178,13 +170,33 @@ export function buildProgram(): Command {
178
170
  ).action(async (query: string[], _local: CommonOpts, command: Command) => {
179
171
  const opts = globalOpts<CommonOpts>(command);
180
172
  const backend = await DatabricksBackend.create(backendOptions(opts));
181
- const resolved = await backend.resolve(query.join(" "));
182
- process.stdout.write(`${JSON.stringify(resolved, null, 2)}\n`);
173
+ const search = query.join(" ");
174
+ const resolved = await backend.resolve(search);
175
+ // Same shape as one `models` entry when matched; null when nothing in the
176
+ // catalogue scores within the threshold (caller can fall back to the query).
177
+ if (!resolved.matched) {
178
+ process.stdout.write("null\n");
179
+ return;
180
+ }
181
+ const endpoint = (await backend.models()).find((e) => e.name === resolved.modelId);
182
+ process.stdout.write(
183
+ `${JSON.stringify(endpoint ? enrichEndpoint(endpoint) : null, null, 2)}\n`,
184
+ );
183
185
  });
184
186
 
185
187
  return program;
186
188
  }
187
189
 
190
+ /** One `models` / `resolve` list entry: endpoint summary + derived capabilities. */
191
+ function enrichEndpoint(endpoint: ServingEndpointSummary) {
192
+ return {
193
+ ...endpoint,
194
+ // `chat` = OpenAI chat/completions + Responses; `embedding` = vectors;
195
+ // `tools` = function/tool calls (every chat endpoint here).
196
+ capabilities: classify.endpointCapabilities(endpoint),
197
+ };
198
+ }
199
+
188
200
  /** Parse `argv` and run the matching command. Throws {@link CommanderError} on flag errors. */
189
201
  export async function runCli(argv: string[]): Promise<void> {
190
202
  await buildProgram().parseAsync(argv);
package/src/server.ts CHANGED
@@ -38,7 +38,8 @@ import { classify, openaiChat, openaiResponses } from "@dbx-tools/shared-model";
38
38
  import type { DatabricksBackend } from "./backend";
39
39
  import { DEFAULT_BIND_HOST, DEFAULT_PORT } from "./defaults";
40
40
 
41
- const { chatToResponsesRequest, responseToChatCompletion } = openaiResponses;
41
+ const { chatToResponsesRequest, responseToChatCompletion, sanitizeResponsesTools } =
42
+ openaiResponses;
42
43
 
43
44
  const logger = log.logger("model-proxy/server");
44
45
 
@@ -329,6 +330,10 @@ async function proxyChatViaResponses(
329
330
  * `POST /v1/responses`: forward the Responses body to Databricks' native
330
331
  * Responses / Open Responses surface (model stays in the body; URL is
331
332
  * workspace-level). Streaming and non-streaming both pass through as-is.
333
+ *
334
+ * Open Responses (Claude/Gemini/…) only accepts `function` tools, so Codex's
335
+ * built-in `web_search` / `local_shell` / … are stripped there. The OpenAI
336
+ * `/responses` path keeps them - GPT models support those tool types.
332
337
  */
333
338
  async function handleResponses(
334
339
  backend: DatabricksBackend,
@@ -345,24 +350,35 @@ async function handleResponses(
345
350
  const resolved = await backend.resolve(requested);
346
351
  body.model = resolved.modelId;
347
352
 
348
- const wantsStream = body.stream === true;
353
+ const upstreamUrl = backend.responsesUrl(resolved.modelId);
354
+ // Cross-provider Open Responses rejects non-function tool types; OpenAI
355
+ // `/responses` keeps Codex built-ins (web_search, local_shell, custom, …).
356
+ const forward = upstreamUrl.includes("/open-responses")
357
+ ? sanitizeResponsesTools(body)
358
+ : body;
359
+
360
+ const wantsStream = forward.stream === true;
349
361
  const headers = await backend.authHeaders();
350
362
  headers["content-type"] = "application/json";
351
363
  headers.accept = wantsStream ? "text/event-stream" : "application/json";
352
364
 
353
- const upstreamUrl = backend.responsesUrl(resolved.modelId);
365
+ const stripped =
366
+ Array.isArray(body.tools) &&
367
+ (!Array.isArray(forward.tools) || forward.tools.length !== body.tools.length);
368
+
354
369
  logger.info("responses", {
355
370
  requested,
356
371
  resolved: resolved.modelId,
357
372
  matched: resolved.matched,
358
373
  upstream: upstreamUrl,
359
374
  stream: wantsStream,
375
+ ...(stripped ? { strippedNonFunctionTools: true } : {}),
360
376
  });
361
377
 
362
378
  const upstream = await fetch(upstreamUrl, {
363
379
  method: "POST",
364
380
  headers,
365
- body: JSON.stringify(body),
381
+ body: JSON.stringify(forward),
366
382
  });
367
383
 
368
384
  res.writeHead(upstream.status, {