@dbx-tools/appkit-model-gateway 0.9.174 → 0.9.176
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 +74 -0
- package/index.ts +4 -1
- package/lib/index.d.ts +4 -1
- package/lib/index.js +4 -2
- package/lib/src/gateway.d.ts +13 -1
- package/lib/src/gateway.js +98 -15
- package/lib/src/models.d.ts +3 -3
- package/lib/src/models.js +100 -6
- package/lib/src/plugin.js +2 -10
- package/lib/src/registry.js +3 -1
- package/lib/src/router.js +27 -5
- package/lib/src/routes.js +5 -2
- package/lib/src/smart-router.d.ts +160 -0
- package/lib/src/smart-router.js +753 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -7
- package/src/gateway.ts +152 -14
- package/src/models.ts +128 -5
- package/src/plugin.ts +1 -9
- package/src/registry.ts +2 -0
- package/src/router.ts +29 -4
- package/src/routes.ts +4 -1
- package/src/smart-router.ts +983 -0
package/lib/tsconfig.tsbuildinfo
CHANGED
|
@@ -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,14 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@databricks/appkit": "0.81.0",
|
|
30
|
-
"@dbx-tools/databricks": "^0.9.
|
|
31
|
-
"@dbx-tools/model": "^0.9.
|
|
32
|
-
"@dbx-tools/model-protocol": "^0.9.
|
|
33
|
-
"@dbx-tools/shared-core": "^0.9.
|
|
34
|
-
"@dbx-tools/shared-model": "^0.9.
|
|
35
|
-
"@dbx-tools/shared-model-gateway": "^0.9.
|
|
30
|
+
"@dbx-tools/databricks": "^0.9.176",
|
|
31
|
+
"@dbx-tools/model": "^0.9.176",
|
|
32
|
+
"@dbx-tools/model-protocol": "^0.9.176",
|
|
33
|
+
"@dbx-tools/shared-core": "^0.9.176",
|
|
34
|
+
"@dbx-tools/shared-model": "^0.9.176",
|
|
35
|
+
"@dbx-tools/shared-model-gateway": "^0.9.176",
|
|
36
36
|
"ai": "^7.0.0",
|
|
37
|
+
"js-tiktoken": "^1.0.21",
|
|
37
38
|
"zod": "4.3.6"
|
|
38
39
|
},
|
|
39
40
|
"main": "./lib/index.js",
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
"./package.json": "./package.json"
|
|
51
52
|
}
|
|
52
53
|
},
|
|
53
|
-
"version": "0.9.
|
|
54
|
+
"version": "0.9.176",
|
|
54
55
|
"types": "./lib/index.d.ts",
|
|
55
56
|
"dbxToolsConfig": {
|
|
56
57
|
"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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
): {
|
|
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)
|
|
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 {
|
|
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(
|
|
60
|
-
|
|
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
|
|
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
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
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:
|
|
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 ||
|
|
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";
|