@dbx-tools/appkit-model-gateway 0.9.175 → 0.9.177

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.
@@ -1 +1 @@
1
- {"root":["../src/gateway.ts","../src/models.ts","../src/plugin.ts","../src/reasoning-adapt.ts","../src/registry.ts","../src/router.ts","../src/routes.ts","../src/translation.ts","../src/transport.ts","../scripts/copy-manifest.ts","../index.ts"],"version":"5.9.3"}
1
+ {"root":["../src/gateway.ts","../src/models.ts","../src/plugin.ts","../src/reasoning-adapt.ts","../src/registry.ts","../src/router.ts","../src/routes.ts","../src/smart-router.ts","../src/translation.ts","../src/transport.ts","../scripts/copy-manifest.ts","../index.ts"],"version":"5.9.3"}
package/package.json CHANGED
@@ -27,13 +27,15 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@databricks/appkit": "0.81.0",
30
- "@dbx-tools/databricks": "^0.9.175",
31
- "@dbx-tools/model": "^0.9.175",
32
- "@dbx-tools/model-protocol": "^0.9.175",
33
- "@dbx-tools/shared-core": "^0.9.175",
34
- "@dbx-tools/shared-model": "^0.9.175",
35
- "@dbx-tools/shared-model-gateway": "^0.9.175",
30
+ "@dbx-tools/cache": "^0.9.177",
31
+ "@dbx-tools/databricks": "^0.9.177",
32
+ "@dbx-tools/model": "^0.9.177",
33
+ "@dbx-tools/model-protocol": "^0.9.177",
34
+ "@dbx-tools/shared-core": "^0.9.177",
35
+ "@dbx-tools/shared-model": "^0.9.177",
36
+ "@dbx-tools/shared-model-gateway": "^0.9.177",
36
37
  "ai": "^7.0.0",
38
+ "js-tiktoken": "^1.0.21",
37
39
  "zod": "4.3.6"
38
40
  },
39
41
  "main": "./lib/index.js",
@@ -50,7 +52,7 @@
50
52
  "./package.json": "./package.json"
51
53
  }
52
54
  },
53
- "version": "0.9.175",
55
+ "version": "0.9.177",
54
56
  "types": "./lib/index.d.ts",
55
57
  "dbxToolsConfig": {
56
58
  "tags": [
package/src/gateway.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  * @module
5
5
  */
6
6
 
7
- import { log } from "@dbx-tools/shared-core";
8
7
  import { modelTemperatureSupportFor } from "@dbx-tools/model";
8
+ import { log } from "@dbx-tools/shared-core";
9
9
  import {
10
10
  ModelClass,
11
11
  ModelClassSchema,
@@ -17,6 +17,20 @@ import type {
17
17
  GatewayRoute,
18
18
  ModelCapabilityOverride,
19
19
  ModelListResponse,
20
+ SmartRouterObservation,
21
+ SmartRouterMode,
22
+ } from "@dbx-tools/shared-model-gateway";
23
+ import {
24
+ MODEL_GATEWAY_DEFAULTS,
25
+ SMART_ROUTER_HARNESS_HEADER,
26
+ SMART_ROUTER_MODEL_ID,
27
+ SMART_ROUTER_PROFILER_STATUS_HEADER,
28
+ SMART_ROUTER_ROUTE_MODEL_HEADER,
29
+ SMART_ROUTER_SELECTED_MODEL_HEADER,
30
+ SMART_ROUTER_SERVED_TIER_HEADER,
31
+ SMART_ROUTER_SOURCE_HEADER,
32
+ SMART_ROUTER_TASK_TIER_HEADER,
33
+ isSmartRouterModelId,
20
34
  } from "@dbx-tools/shared-model-gateway";
21
35
 
22
36
  import { listModelsPayload } from "./models.ts";
@@ -27,6 +41,11 @@ import {
27
41
  type ModelRegistryOptions,
28
42
  } from "./registry.ts";
29
43
  import { isCodexOriginator, requestedFeatures, resolveRoute } from "./router.ts";
44
+ import {
45
+ SmartModelRouter,
46
+ type SmartRouterService,
47
+ type SmartRouterStore,
48
+ } from "./smart-router.ts";
30
49
  import { translateGatewayRequest } from "./translation.ts";
31
50
  import { fetchDatabricks, gatewayResponseHeaders } from "./transport.ts";
32
51
 
@@ -37,6 +56,14 @@ const DATABRICKS_TEMPERATURE_MAX = 2;
37
56
  /** Model gateway runtime configuration. */
38
57
  export interface ModelGatewayOptions {
39
58
  readonly cacheTtlMs?: number;
59
+ readonly smartRouter?: SmartRouterMode;
60
+ readonly smartRouterContextTokenBudget?: number;
61
+ readonly smartRouterProbeRefreshIntervalMs?: number;
62
+ readonly smartRouterProbeTtlMs?: number;
63
+ readonly smartRouterRouteTtlSeconds?: number;
64
+ readonly smartRouterTimeoutMs?: number;
65
+ readonly smartRouterService?: SmartRouterService;
66
+ readonly smartRouterStore?: SmartRouterStore;
40
67
  readonly overrides?: readonly ModelCapabilityOverride[];
41
68
  readonly model?: string;
42
69
  readonly modelClass?: ModelClassType;
@@ -57,6 +84,8 @@ export class ModelGatewayError extends Error {
57
84
  /** AppKit-scoped model gateway with dynamic model discovery and streaming inference. */
58
85
  export class ModelGateway {
59
86
  private readonly registry: ModelRegistry;
87
+ private readonly smartRouter?: SmartModelRouter;
88
+ private readonly smartRouterMode: SmartRouterMode;
60
89
  private readonly model?: string;
61
90
  private readonly modelClass?: ModelClassType;
62
91
  private readonly preferSystemModels: boolean;
@@ -70,9 +99,30 @@ export class ModelGateway {
70
99
  : {}),
71
100
  };
72
101
  this.registry = registry ?? new DatabricksModelRegistry(registryOptions);
102
+ this.smartRouterMode = options.smartRouter ?? MODEL_GATEWAY_DEFAULTS.smartRouter;
103
+ if (this.smartRouterMode !== "false") {
104
+ this.smartRouter = new SmartModelRouter(
105
+ {
106
+ contextTokenBudget:
107
+ options.smartRouterContextTokenBudget ??
108
+ MODEL_GATEWAY_DEFAULTS.smartRouterContextTokenBudget,
109
+ probeRefreshIntervalMs:
110
+ options.smartRouterProbeRefreshIntervalMs ??
111
+ MODEL_GATEWAY_DEFAULTS.smartRouterProbeRefreshIntervalMs,
112
+ probeTtlMs: options.smartRouterProbeTtlMs ?? MODEL_GATEWAY_DEFAULTS.smartRouterProbeTtlMs,
113
+ routeTtlSeconds:
114
+ options.smartRouterRouteTtlSeconds ?? MODEL_GATEWAY_DEFAULTS.smartRouterRouteTtlSeconds,
115
+ routerName: "task_v3",
116
+ timeoutMs: options.smartRouterTimeoutMs ?? MODEL_GATEWAY_DEFAULTS.smartRouterTimeoutMs,
117
+ },
118
+ options.smartRouterService,
119
+ options.smartRouterStore,
120
+ );
121
+ }
73
122
  this.model = options.model;
74
123
  this.modelClass = options.modelClass;
75
- this.preferSystemModels = options.preferSystemModels ?? false;
124
+ this.preferSystemModels =
125
+ options.preferSystemModels ?? MODEL_GATEWAY_DEFAULTS.preferSystemModels;
76
126
  }
77
127
 
78
128
  /** Return the live OpenAI model list, including Codex metadata when requested. */
@@ -80,14 +130,33 @@ export class ModelGateway {
80
130
  const codex = isCodexOriginator(originator);
81
131
  logger.debug("listing models", { codex, search });
82
132
  const query = search?.trim();
83
- const targets = query ? await this.registry.search(query) : await this.registry.list();
133
+ const smartRouterSearch =
134
+ query !== undefined && SMART_ROUTER_MODEL_ID.includes(query.toLowerCase());
135
+ const targets =
136
+ query && !smartRouterSearch ? await this.registry.search(query) : await this.registry.list();
137
+ const smartRouterTargets =
138
+ this.smartRouterMode === "true" && query && !smartRouterSearch
139
+ ? await this.registry.list()
140
+ : targets;
141
+ const shouldProbeSmartRouter =
142
+ Boolean(this.smartRouter) && (this.smartRouterMode === "true" || !query || smartRouterSearch);
143
+ const smartRouterEnabled =
144
+ shouldProbeSmartRouter && (await this.smartRouter!.isEnabled(smartRouterTargets));
145
+ if (this.smartRouterMode === "true" && !smartRouterEnabled) {
146
+ throw new ModelGatewayError(
147
+ 503,
148
+ "Databricks Smart Router is required but unavailable for the active workspace identity",
149
+ );
150
+ }
84
151
  logger.debug("listed models", { codex, count: targets.length });
85
- return listModelsPayload(
152
+ const payload = listModelsPayload(
86
153
  targets,
87
154
  codex,
88
155
  query !== undefined && query.length > 0,
89
156
  this.preferSystemModels,
157
+ smartRouterEnabled && (!query || smartRouterSearch),
90
158
  );
159
+ return smartRouterSearch ? onlySmartRouter(payload) : payload;
91
160
  }
92
161
 
93
162
  /** Resolve and execute one inference request. */
@@ -108,6 +177,7 @@ export class ModelGateway {
108
177
  protocol,
109
178
  );
110
179
  logger.debug("resolving request", {
180
+ smartRouter: selection.smartRouter === true,
111
181
  clientProtocol: protocol,
112
182
  model: selection.model,
113
183
  modelClass: selection.modelClass,
@@ -115,11 +185,31 @@ export class ModelGateway {
115
185
  stream: sanitizedBody.stream === true,
116
186
  });
117
187
  const features = requestedFeatures(sanitizedBody);
118
- const target = await this.registry.resolve(selection.model, {
119
- ...(protocol === "openai-embeddings" ? { modelClass: ModelClass.Embedding } : {}),
120
- ...(selection.modelClass ? { modelClass: selection.modelClass } : {}),
121
- ...(features.tools ? { requiresTools: true } : {}),
122
- });
188
+ if (selection.smartRouter && protocol === "openai-embeddings") {
189
+ throw new ModelGatewayError(400, "Model smart-router is available only for chat requests");
190
+ }
191
+ const smartRouter =
192
+ selection.smartRouter && this.smartRouter
193
+ ? await this.smartRouter.resolve({
194
+ protocol: protocol as Exclude<ClientProtocol, "openai-embeddings">,
195
+ body: sanitizedBody,
196
+ headers,
197
+ features,
198
+ targets: await this.registry.list(),
199
+ preferSystemModels: this.preferSystemModels,
200
+ signal,
201
+ })
202
+ : undefined;
203
+ if (selection.smartRouter && !smartRouter) {
204
+ throw new ModelGatewayError(404, "Databricks Smart Router is disabled");
205
+ }
206
+ const target =
207
+ smartRouter?.target ??
208
+ (await this.registry.resolve(selection.model, {
209
+ ...(protocol === "openai-embeddings" ? { modelClass: ModelClass.Embedding } : {}),
210
+ ...(selection.modelClass ? { modelClass: selection.modelClass } : {}),
211
+ ...(features.tools ? { requiresTools: true } : {}),
212
+ }));
123
213
  if (!target) {
124
214
  throw new ModelGatewayError(
125
215
  404,
@@ -130,7 +220,7 @@ export class ModelGateway {
130
220
  }
131
221
  const route = resolveRoute({
132
222
  clientProtocol: protocol,
133
- ...(selection.model ? { requestedModel: selection.model } : {}),
223
+ ...(!smartRouter && selection.model ? { requestedModel: selection.model } : {}),
134
224
  originator: headers.get("originator") ?? undefined,
135
225
  preferSystemModels: this.preferSystemModels,
136
226
  features,
@@ -144,7 +234,7 @@ export class ModelGateway {
144
234
 
145
235
  const temperatureBody = adaptInferenceTemperature(
146
236
  target.endpoint ?? target.id,
147
- sanitizedBody,
237
+ smartRouter?.body ?? sanitizedBody,
148
238
  );
149
239
  const adapted = adaptInferenceReasoning(target.id, temperatureBody);
150
240
  let response = await this.forward(route, adapted.body, headers, signal);
@@ -162,10 +252,14 @@ export class ModelGateway {
162
252
  logResponse(route.upstreamProtocol, target.id, response);
163
253
  if (route.upstreamProtocol === "ai-sdk") {
164
254
  response.headers.set("x-dbx-tools-upstream", route.upstreamProtocol);
255
+ if (smartRouter) {
256
+ applySmartRouterObservationHeaders(response.headers, smartRouter.observation);
257
+ }
165
258
  return response;
166
259
  }
167
260
  const forwarded = gatewayResponseHeaders(response.headers);
168
261
  forwarded.set("x-dbx-tools-upstream", route.upstreamProtocol);
262
+ if (smartRouter) applySmartRouterObservationHeaders(forwarded, smartRouter.observation);
169
263
  return new Response(response.body, {
170
264
  status: response.status,
171
265
  statusText: response.statusText,
@@ -234,7 +328,11 @@ export function gatewayModelSelection(
234
328
  headers: Headers,
235
329
  defaults: { readonly model?: string; readonly modelClass?: ModelClassType } = {},
236
330
  protocol?: ClientProtocol,
237
- ): { readonly model?: string; readonly modelClass?: ModelClassType } {
331
+ ): {
332
+ readonly smartRouter?: boolean;
333
+ readonly model?: string;
334
+ readonly modelClass?: ModelClassType;
335
+ } {
238
336
  const model = typeof body.model === "string" && body.model.trim() ? body.model.trim() : undefined;
239
337
  const header = headers.get("x-dbx-tools-model-class")?.trim();
240
338
  const parsedClass = header ? ModelClassSchema.safeParse(header) : undefined;
@@ -244,14 +342,54 @@ export function gatewayModelSelection(
244
342
  if (model && parsedClass?.data) {
245
343
  throw new ModelGatewayError(400, "Model and model class are mutually exclusive");
246
344
  }
247
- if (model) return { model };
345
+ if (model) return isSmartRouterModelId(model) ? { smartRouter: true } : { model };
248
346
  if (parsedClass?.data) return { modelClass: parsedClass.data };
249
- if (defaults.model) return { model: defaults.model };
347
+ if (defaults.model) {
348
+ return isSmartRouterModelId(defaults.model) ? { smartRouter: true } : { model: defaults.model };
349
+ }
250
350
  if (defaults.modelClass) return { modelClass: defaults.modelClass };
251
351
  if (protocol === "openai-embeddings") return { modelClass: ModelClass.Embedding };
252
352
  return {};
253
353
  }
254
354
 
355
+ function applySmartRouterObservationHeaders(
356
+ headers: Headers,
357
+ observation: SmartRouterObservation,
358
+ ): void {
359
+ headers.set(SMART_ROUTER_SELECTED_MODEL_HEADER, observation.selectedModel);
360
+ headers.set(SMART_ROUTER_ROUTE_MODEL_HEADER, observation.routeModel);
361
+ headers.set(SMART_ROUTER_HARNESS_HEADER, observation.harness);
362
+ headers.set(SMART_ROUTER_SOURCE_HEADER, observation.source);
363
+ const taskTier = stringParam(observation.params, "task_tier");
364
+ const servedTier = stringParam(observation.params, "served_tier");
365
+ const profilerStatus = stringParam(observation.params, "profiler_status");
366
+ if (taskTier) headers.set(SMART_ROUTER_TASK_TIER_HEADER, taskTier);
367
+ if (servedTier) headers.set(SMART_ROUTER_SERVED_TIER_HEADER, servedTier);
368
+ if (profilerStatus) headers.set(SMART_ROUTER_PROFILER_STATUS_HEADER, profilerStatus);
369
+ }
370
+
371
+ function stringParam(
372
+ params: Readonly<Record<string, unknown>> | undefined,
373
+ key: string,
374
+ ): string | undefined {
375
+ const value = params?.[key];
376
+ return typeof value === "string" && value ? value : undefined;
377
+ }
378
+
379
+ function onlySmartRouter(payload: ModelListResponse): ModelListResponse {
380
+ if ("models" in payload) {
381
+ return {
382
+ models: payload.models.filter((model) =>
383
+ model.slug.toLowerCase().endsWith(`/${SMART_ROUTER_MODEL_ID}`),
384
+ ),
385
+ };
386
+ }
387
+ return {
388
+ object: "list",
389
+ data: payload.data.filter((model) => model.id === SMART_ROUTER_MODEL_ID),
390
+ };
391
+ }
392
+
255
393
  function logResponse(upstreamProtocol: string, model: string, response: Response): void {
256
394
  const details = {
257
395
  contentType: response.headers.get("content-type"),
package/src/models.ts CHANGED
@@ -4,10 +4,22 @@
4
4
  * @module
5
5
  */
6
6
 
7
- import { isChatClass, modelFamily, modelFamilyRank } from "@dbx-tools/model";
7
+ import {
8
+ CHAT_CLASS_ORDER,
9
+ compareVersionTuples,
10
+ isChatClass,
11
+ isOpenWeightsGpt,
12
+ ModelFamily,
13
+ modelFamily,
14
+ modelFamilyRank,
15
+ versionTuple,
16
+ } from "@dbx-tools/model";
17
+ import { ModelClass, ReasoningEffort } from "@dbx-tools/shared-model";
18
+ import { SMART_ROUTER_MODEL_ID } from "@dbx-tools/shared-model-gateway";
8
19
  import type {
9
20
  CodexModel,
10
21
  CodexModelListResponse,
22
+ ModelCapabilities,
11
23
  ModelListResponse,
12
24
  ModelTarget,
13
25
  OpenAIModel,
@@ -16,6 +28,12 @@ import type {
16
28
 
17
29
  const CODEX_BASE_INSTRUCTIONS =
18
30
  "You are a coding agent. Follow the user's instructions and use the available tools to work in the current repository.";
31
+ const CODEX_SMART_ROUTER_PRIORITY = 9_000;
32
+ const CODEX_MODEL_PRIORITY_BASE = 10_000;
33
+ const CODEX_FAMILY_ORDER = [
34
+ ModelFamily.Gpt,
35
+ ...Object.values(ModelFamily).filter((family) => family !== ModelFamily.Gpt),
36
+ ] as const;
19
37
 
20
38
  const REASONING_DESCRIPTIONS: Readonly<Record<string, string>> = {
21
39
  none: "Disable explicit reasoning",
@@ -33,36 +51,56 @@ export function listModelsPayload(
33
51
  includeCodex: true,
34
52
  preserveOrder?: boolean,
35
53
  preferSystemModels?: boolean,
54
+ includeSmartRouter?: boolean,
36
55
  ): CodexModelListResponse;
37
56
  export function listModelsPayload(
38
57
  targets: readonly ModelTarget[],
39
58
  includeCodex: false,
40
59
  preserveOrder?: boolean,
41
60
  preferSystemModels?: boolean,
61
+ includeSmartRouter?: boolean,
42
62
  ): OpenAIModelListResponse;
43
63
  export function listModelsPayload(
44
64
  targets: readonly ModelTarget[],
45
65
  includeCodex: boolean,
46
66
  preserveOrder?: boolean,
47
67
  preferSystemModels?: boolean,
68
+ includeSmartRouter?: boolean,
48
69
  ): ModelListResponse;
49
70
  export function listModelsPayload(
50
71
  targets: readonly ModelTarget[],
51
72
  includeCodex: boolean,
52
73
  preserveOrder = false,
53
74
  preferSystemModels = false,
75
+ includeSmartRouter = false,
54
76
  ): ModelListResponse {
55
77
  const available = targets
56
78
  .filter((target) => target.endpoint?.status?.deprecated !== true)
57
79
  .filter((target) => !includeCodex || hasChatClass(target));
58
80
  if (!preserveOrder) {
59
- available.sort((left, right) =>
60
- compareCatalogueTargets(left, right, (target) => target.displayName),
81
+ available.sort(
82
+ includeCodex
83
+ ? compareCodexCatalogueTargets
84
+ : (left, right) =>
85
+ compareCatalogueTargets(left, right, (target) => target.displayName),
61
86
  );
62
87
  }
88
+ const smartRouterCandidates = available.filter((target) => target.capabilities.chat);
89
+ if (includeSmartRouter && smartRouterCandidates.length > 0) {
90
+ available.unshift(smartRouterModelTarget(smartRouterCandidates));
91
+ }
63
92
  if (includeCodex) {
93
+ let modelIndex = 0;
64
94
  return {
65
- models: available.map((target, index) => codexModel(target, index + 1, preferSystemModels)),
95
+ models: available.map((target) =>
96
+ codexModel(
97
+ target,
98
+ target.id === SMART_ROUTER_MODEL_ID
99
+ ? CODEX_SMART_ROUTER_PRIORITY
100
+ : CODEX_MODEL_PRIORITY_BASE + modelIndex++,
101
+ preferSystemModels,
102
+ ),
103
+ ),
66
104
  };
67
105
  }
68
106
  return {
@@ -81,6 +119,7 @@ function openAiModel(target: ModelTarget): OpenAIModel {
81
119
  task: target.endpoint?.task,
82
120
  status: target.endpoint?.status ?? { deprecated: false },
83
121
  capabilities: {
122
+ smart_router: target.id === SMART_ROUTER_MODEL_ID,
84
123
  tools: target.capabilities.tools,
85
124
  reasoning: target.reasoningEfforts,
86
125
  responses: target.capabilities.responses,
@@ -90,10 +129,52 @@ function openAiModel(target: ModelTarget): OpenAIModel {
90
129
  ai_gateway_codex: target.capabilities.aiGatewayCodex,
91
130
  streaming: target.capabilities.streaming,
92
131
  web_search: target.capabilities.webSearch,
132
+ image_input: target.capabilities.imageInput,
133
+ apply_patch: target.capabilities.applyPatch,
93
134
  },
94
135
  };
95
136
  }
96
137
 
138
+ function smartRouterModelTarget(candidates: readonly ModelTarget[]): ModelTarget {
139
+ const has = (capability: keyof ModelCapabilities) =>
140
+ candidates.some((target) => target.capabilities[capability]);
141
+ const reasoningEfforts = Object.values(ReasoningEffort).filter((effort) =>
142
+ candidates.some((target) => target.reasoningEfforts.includes(effort)),
143
+ );
144
+ return {
145
+ id: SMART_ROUTER_MODEL_ID,
146
+ aliases: [SMART_ROUTER_MODEL_ID],
147
+ displayName: "Smart Router",
148
+ endpoint: {
149
+ name: SMART_ROUTER_MODEL_ID,
150
+ displayName: "Smart Router",
151
+ description:
152
+ "Uses Databricks task_v3 Smart Router to select a compatible system model for each turn.",
153
+ task: "llm/v1/chat",
154
+ class: ModelClass.ChatBalanced,
155
+ status: { deprecated: false },
156
+ },
157
+ capabilities: {
158
+ responses: true,
159
+ openResponses: true,
160
+ chat: true,
161
+ anthropic: true,
162
+ embeddings: false,
163
+ aiGatewayCodex: true,
164
+ tools: has("tools"),
165
+ reasoning: reasoningEfforts.length > 0,
166
+ streaming: true,
167
+ parallelTools: has("parallelTools"),
168
+ customTools: has("customTools"),
169
+ structuredOutput: has("structuredOutput"),
170
+ webSearch: has("webSearch"),
171
+ imageInput: has("imageInput"),
172
+ applyPatch: has("applyPatch"),
173
+ },
174
+ reasoningEfforts,
175
+ };
176
+ }
177
+
97
178
  /** Codex `/v1/models` slug: the `system.ai.*` service when preferred, else the endpoint id. */
98
179
  export function codexModelSlug(target: ModelTarget, preferSystemModels: boolean): string {
99
180
  const model =
@@ -111,7 +192,7 @@ function codexModel(
111
192
  const efforts = target.reasoningEfforts;
112
193
  return {
113
194
  slug: codexModelSlug(target, preferSystemModels),
114
- display_name: target.displayName,
195
+ display_name: codexDisplayName(target),
115
196
  description: target.endpoint?.description ?? "Databricks model service",
116
197
  base_instructions: CODEX_BASE_INSTRUCTIONS,
117
198
  status: target.endpoint?.status ?? { deprecated: false },
@@ -141,6 +222,48 @@ function codexModel(
141
222
  };
142
223
  }
143
224
 
225
+ function codexDisplayName(target: ModelTarget): string {
226
+ if (catalogueFamily(target) !== ModelFamily.Gpt) return target.displayName;
227
+ const displayName = target.displayName.replace(/^gpt(?=[\s._-])/i, "GPT");
228
+ return `Databricks ${displayName}`;
229
+ }
230
+
231
+ function compareCodexCatalogueTargets(left: ModelTarget, right: ModelTarget): number {
232
+ const familyOrder = codexFamilyRank(left) - codexFamilyRank(right);
233
+ if (familyOrder !== 0) return familyOrder;
234
+ const modelKindOrder = codexModelKindRank(left) - codexModelKindRank(right);
235
+ if (modelKindOrder !== 0) return modelKindOrder;
236
+ const versionOrder = compareVersionTuples(codexVersion(right), codexVersion(left));
237
+ if (versionOrder !== 0) return versionOrder;
238
+ const classOrder = codexClassRank(left) - codexClassRank(right);
239
+ if (classOrder !== 0) return classOrder;
240
+ return compareCatalogueLabel(left.displayName, right.displayName);
241
+ }
242
+
243
+ function codexModelKindRank(target: ModelTarget): number {
244
+ return [target.displayName, target.id, target.modelServiceName].some(
245
+ (identity) => identity && isOpenWeightsGpt(identity),
246
+ )
247
+ ? 1
248
+ : 0;
249
+ }
250
+
251
+ function codexFamilyRank(target: ModelTarget): number {
252
+ const family = catalogueFamily(target);
253
+ const index = family ? CODEX_FAMILY_ORDER.indexOf(family as ModelFamily) : -1;
254
+ return index >= 0 ? index : CODEX_FAMILY_ORDER.length;
255
+ }
256
+
257
+ function codexVersion(target: ModelTarget): readonly number[] {
258
+ return versionTuple(target.displayName);
259
+ }
260
+
261
+ function codexClassRank(target: ModelTarget): number {
262
+ const modelClass = target.endpoint?.class;
263
+ const index = modelClass ? CHAT_CLASS_ORDER.indexOf(modelClass) : -1;
264
+ return index >= 0 ? index : CHAT_CLASS_ORDER.length;
265
+ }
266
+
144
267
  /**
145
268
  * Order listed models in three groups: major-provider chat families, the
146
269
  * remaining chat models, then embeddings. Families keep a fixed provider
package/src/plugin.ts CHANGED
@@ -29,15 +29,7 @@ export class ModelGatewayPlugin extends Plugin<ModelGatewayPluginConfig> {
29
29
 
30
30
  constructor(config: ModelGatewayPluginConfig = {}) {
31
31
  super(config);
32
- this.gateway = new ModelGateway({
33
- ...(config.cacheTtlMs !== undefined ? { cacheTtlMs: config.cacheTtlMs } : {}),
34
- ...(config.overrides ? { overrides: config.overrides } : {}),
35
- ...(config.model ? { model: config.model } : {}),
36
- ...(config.modelClass ? { modelClass: config.modelClass } : {}),
37
- ...(config.preferSystemModels !== undefined
38
- ? { preferSystemModels: config.preferSystemModels }
39
- : {}),
40
- });
32
+ this.gateway = new ModelGateway(config);
41
33
  }
42
34
 
43
35
  override injectRoutes(router: IAppRouter): void {
package/src/registry.ts CHANGED
@@ -202,6 +202,8 @@ export class DatabricksModelRegistry implements ModelRegistry {
202
202
  customTools: published.applyPatch || endpoint.name.includes("codex"),
203
203
  structuredOutput: true,
204
204
  webSearch: published.webSearch,
205
+ imageInput: published.imageInput,
206
+ applyPatch: published.applyPatch,
205
207
  };
206
208
  const aliases = new Set(
207
209
  [
package/src/router.ts CHANGED
@@ -74,18 +74,31 @@ export function resolveRoute(input: ResolveRouteInput): GatewayRoute {
74
74
  /** Detect route-affecting features from an OpenAI or Anthropic request body. */
75
75
  export function requestedFeatures(body: Readonly<Record<string, unknown>>): RequestedFeatures {
76
76
  const tools = Array.isArray(body.tools) ? body.tools : [];
77
- const toolTypes = tools
78
- .filter((tool): tool is Record<string, unknown> => isRecord(tool))
79
- .map((tool) => tool.type);
77
+ const toolRecords = tools.filter((tool): tool is Record<string, unknown> => isRecord(tool));
78
+ const toolTypes = toolRecords.map((tool) => tool.type);
79
+ const toolNames = toolRecords
80
+ .map((tool) =>
81
+ typeof tool.name === "string"
82
+ ? tool.name
83
+ : isRecord(tool.function) && typeof tool.function.name === "string"
84
+ ? tool.function.name
85
+ : undefined,
86
+ )
87
+ .filter((name): name is string => name !== undefined);
80
88
  const text = isRecord(body.text) ? body.text : {};
81
89
  const format = isRecord(text.format) ? text.format.type : text.format;
90
+ const outputConfig = isRecord(body.output_config) ? body.output_config : {};
82
91
  return {
83
92
  background: body.background === true,
84
93
  customTools: toolTypes.some((type) => type === "custom"),
85
94
  parallelTools: body.parallel_tool_calls === true,
86
95
  previousResponse:
87
96
  typeof body.previous_response_id === "string" && body.previous_response_id.length > 0,
88
- reasoning: isRecord(body.reasoning) || body.reasoning_effort !== undefined,
97
+ reasoning:
98
+ isRecord(body.reasoning) ||
99
+ body.reasoning_effort !== undefined ||
100
+ isRecord(body.thinking) ||
101
+ outputConfig.effort !== undefined,
89
102
  storage: body.store === true,
90
103
  structuredOutput: format !== undefined && format !== "text",
91
104
  tools: tools.length > 0,
@@ -93,6 +106,8 @@ export function requestedFeatures(body: Readonly<Record<string, unknown>>): Requ
93
106
  (type) => type !== "function" && type !== "web_search" && type !== "web_search_preview",
94
107
  ),
95
108
  webSearch: toolTypes.some((type) => type === "web_search" || type === "web_search_preview"),
109
+ imageInput: containsImageInput(body.input) || containsImageInput(body.messages),
110
+ applyPatch: toolNames.some((name) => /^apply[_-]?patch$/i.test(name)),
96
111
  };
97
112
  }
98
113
 
@@ -170,3 +185,13 @@ function canUseAiGatewayCodex(target: ModelTarget, features: RequestedFeatures):
170
185
  supportsDirectResponses(target, features)
171
186
  );
172
187
  }
188
+
189
+ function containsImageInput(value: unknown): boolean {
190
+ if (Array.isArray(value)) return value.some(containsImageInput);
191
+ if (!isRecord(value)) return false;
192
+ const type = typeof value.type === "string" ? value.type.toLowerCase() : "";
193
+ if (type.includes("image")) return true;
194
+ return Object.entries(value).some(
195
+ ([key, nested]) => key !== "arguments" && key !== "input" && containsImageInput(nested),
196
+ );
197
+ }
package/src/routes.ts CHANGED
@@ -12,6 +12,7 @@ import type express from "express";
12
12
 
13
13
  import { ModelGateway, ModelGatewayError } from "./gateway.ts";
14
14
  import { UnsupportedGatewayFeatureError } from "./router.ts";
15
+ import { SmartRouterError } from "./smart-router.ts";
15
16
 
16
17
  const logger = log.logger("appkit/model-gateway/http");
17
18
 
@@ -128,7 +129,9 @@ export async function sendInference(
128
129
 
129
130
  function sendError(protocol: ClientProtocol, response: express.Response, error: unknown): void {
130
131
  const status =
131
- error instanceof ModelGatewayError || error instanceof UnsupportedGatewayFeatureError
132
+ error instanceof ModelGatewayError ||
133
+ error instanceof SmartRouterError ||
134
+ error instanceof UnsupportedGatewayFeatureError
132
135
  ? error.status
133
136
  : 500;
134
137
  const message = error instanceof Error ? error.message : "Model gateway request failed";