@dbx-tools/appkit-model-gateway 0.9.60 → 0.9.64
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/index.ts +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +2 -2
- package/lib/src/gateway.d.ts +13 -0
- package/lib/src/gateway.js +41 -11
- package/lib/src/models.d.ts +2 -0
- package/lib/src/models.js +7 -3
- package/lib/src/plugin.js +3 -1
- package/lib/src/registry.d.ts +7 -2
- package/lib/src/registry.js +29 -16
- package/lib/src/router.js +11 -12
- package/lib/src/transport.d.ts +2 -0
- package/lib/src/transport.js +14 -2
- package/package.json +6 -6
- package/src/gateway.ts +54 -9
- package/src/models.ts +7 -2
- package/src/plugin.ts +2 -0
- package/src/registry.ts +54 -22
- package/src/router.ts +14 -13
- package/src/transport.ts +16 -1
package/src/registry.ts
CHANGED
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { getExecutionContext, type WorkspaceClient } from "@databricks/appkit";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
metadata,
|
|
10
|
+
modelCatalog,
|
|
11
|
+
policy,
|
|
12
|
+
resolve as modelResolve,
|
|
13
|
+
type ListServingEndpointsOptions,
|
|
14
|
+
} from "@dbx-tools/model";
|
|
9
15
|
import { log } from "@dbx-tools/shared-core";
|
|
10
16
|
import {
|
|
11
17
|
ModelClass,
|
|
@@ -28,7 +34,7 @@ export type ModelRegistryResolveOptions = Pick<ResolveModelInput, "modelClass" |
|
|
|
28
34
|
export interface ModelRegistry {
|
|
29
35
|
list(): Promise<ModelTarget[]>;
|
|
30
36
|
search(query: string): Promise<ModelTarget[]>;
|
|
31
|
-
resolve(model
|
|
37
|
+
resolve(model?: string, options?: ModelRegistryResolveOptions): Promise<ModelTarget | undefined>;
|
|
32
38
|
refresh(): Promise<void>;
|
|
33
39
|
}
|
|
34
40
|
|
|
@@ -36,31 +42,36 @@ export interface ModelRegistry {
|
|
|
36
42
|
export interface ModelRegistryOptions {
|
|
37
43
|
readonly ttlMs?: number;
|
|
38
44
|
readonly overrides?: readonly ModelCapabilityOverride[];
|
|
45
|
+
/** Discover models with this client instead of the AppKit execution context. */
|
|
46
|
+
readonly client?: WorkspaceClient;
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
interface RegistryContext {
|
|
42
50
|
readonly client: WorkspaceClient;
|
|
43
51
|
readonly host: string;
|
|
44
|
-
readonly identity
|
|
52
|
+
readonly identity?: string;
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
/** Model registry backed by the active AppKit execution context. */
|
|
48
56
|
export class DatabricksModelRegistry implements ModelRegistry {
|
|
49
57
|
private readonly ttlMs: number;
|
|
50
58
|
private readonly overrides: readonly ModelCapabilityOverride[];
|
|
59
|
+
private readonly client: WorkspaceClient | undefined;
|
|
51
60
|
|
|
52
61
|
constructor(options: ModelRegistryOptions = {}) {
|
|
53
62
|
this.ttlMs = positiveTtl(options.ttlMs);
|
|
54
63
|
this.overrides = options.overrides ?? [];
|
|
64
|
+
this.client = options.client;
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
async list(): Promise<ModelTarget[]> {
|
|
58
|
-
const context = await
|
|
68
|
+
const context = await this.context();
|
|
59
69
|
logger.debug("loading catalogue", { host: context.host, ttlMs: this.ttlMs });
|
|
60
|
-
const endpoints = await modelCatalog.listServingEndpoints(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
const endpoints = await modelCatalog.listServingEndpoints(
|
|
71
|
+
context.client,
|
|
72
|
+
context.host,
|
|
73
|
+
catalogueOptions(context, this.ttlMs),
|
|
74
|
+
);
|
|
64
75
|
const targets = endpoints.map((endpoint) => this.target(endpoint));
|
|
65
76
|
logger.debug("loaded catalogue", {
|
|
66
77
|
endpointCount: endpoints.length,
|
|
@@ -71,16 +82,19 @@ export class DatabricksModelRegistry implements ModelRegistry {
|
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
async resolve(
|
|
74
|
-
model: string,
|
|
85
|
+
model: string | undefined,
|
|
75
86
|
options: ModelRegistryResolveOptions = {},
|
|
76
87
|
): Promise<ModelTarget | undefined> {
|
|
77
|
-
const requested = unqualifiedModel(model);
|
|
88
|
+
const requested = model === undefined ? undefined : unqualifiedModel(model);
|
|
78
89
|
const targets = await this.list();
|
|
79
|
-
const exact =
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
const exact =
|
|
91
|
+
requested === undefined
|
|
92
|
+
? undefined
|
|
93
|
+
: targets.find((target) =>
|
|
94
|
+
target.aliases.some(
|
|
95
|
+
(alias) => alias.localeCompare(requested, undefined, { sensitivity: "accent" }) === 0,
|
|
96
|
+
),
|
|
97
|
+
);
|
|
84
98
|
if (exact && matchesResolveOptions(exact, options)) {
|
|
85
99
|
logger.debug("resolved exact model", { requested, resolved: exact.id });
|
|
86
100
|
return exact;
|
|
@@ -90,7 +104,7 @@ export class DatabricksModelRegistry implements ModelRegistry {
|
|
|
90
104
|
.map((target) => target.endpoint)
|
|
91
105
|
.filter((endpoint): endpoint is ServingEndpointSummary => endpoint !== undefined);
|
|
92
106
|
const resolved = modelResolve.resolveModel(endpoints, {
|
|
93
|
-
explicit: requested,
|
|
107
|
+
...(requested !== undefined ? { explicit: requested } : {}),
|
|
94
108
|
...options,
|
|
95
109
|
});
|
|
96
110
|
const target = targets.find((candidate) => candidate.id === resolved.modelId);
|
|
@@ -124,13 +138,16 @@ export class DatabricksModelRegistry implements ModelRegistry {
|
|
|
124
138
|
}
|
|
125
139
|
|
|
126
140
|
async refresh(): Promise<void> {
|
|
127
|
-
const context = await
|
|
141
|
+
const context = await this.context();
|
|
128
142
|
logger.debug("clearing catalogue", { host: context.host });
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
if (context.identity) {
|
|
144
|
+
await modelCatalog.clearServingEndpointsCache(context.host, context.identity);
|
|
145
|
+
}
|
|
146
|
+
await modelCatalog.listServingEndpoints(
|
|
147
|
+
context.client,
|
|
148
|
+
context.host,
|
|
149
|
+
catalogueOptions(context, this.ttlMs),
|
|
150
|
+
);
|
|
134
151
|
logger.debug("refreshed catalogue", { host: context.host });
|
|
135
152
|
}
|
|
136
153
|
|
|
@@ -182,6 +199,21 @@ export class DatabricksModelRegistry implements ModelRegistry {
|
|
|
182
199
|
reasoningEfforts: endpoint.reasoningEfforts ?? [],
|
|
183
200
|
};
|
|
184
201
|
}
|
|
202
|
+
|
|
203
|
+
private async context(): Promise<RegistryContext> {
|
|
204
|
+
if (this.client) {
|
|
205
|
+
const host = (await this.client.config.getHost()).toString();
|
|
206
|
+
return { client: this.client, host };
|
|
207
|
+
}
|
|
208
|
+
return registryContext();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function catalogueOptions(context: RegistryContext, ttlMs: number): ListServingEndpointsOptions {
|
|
213
|
+
return {
|
|
214
|
+
ttlMs,
|
|
215
|
+
...(context.identity ? { cacheIdentity: context.identity } : {}),
|
|
216
|
+
};
|
|
185
217
|
}
|
|
186
218
|
|
|
187
219
|
async function registryContext(): Promise<RegistryContext> {
|
package/src/router.ts
CHANGED
|
@@ -36,21 +36,16 @@ export function resolveRoute(input: ResolveRouteInput): GatewayRoute {
|
|
|
36
36
|
rejectStatefulResponses(features);
|
|
37
37
|
if (
|
|
38
38
|
isCodexOriginator(input.originator) &&
|
|
39
|
-
|
|
40
|
-
target
|
|
41
|
-
supportsDirectResponses(target, features)
|
|
39
|
+
canUseAiGatewayCodex(target, features) &&
|
|
40
|
+
!requestedServingEndpoint(requestedModel, target)
|
|
42
41
|
) {
|
|
43
42
|
return directRoute(input, "databricks-ai-gateway-codex", target.modelServiceName!);
|
|
44
43
|
}
|
|
45
44
|
if (target.capabilities.responses && supportsDirectResponses(target, features)) {
|
|
46
45
|
return directRoute(input, "databricks-responses", target.id);
|
|
47
46
|
}
|
|
48
|
-
if (
|
|
49
|
-
target.
|
|
50
|
-
target.modelServiceName &&
|
|
51
|
-
supportsDirectResponses(target, features)
|
|
52
|
-
) {
|
|
53
|
-
return directRoute(input, "databricks-ai-gateway-codex", target.modelServiceName);
|
|
47
|
+
if (canUseAiGatewayCodex(target, features)) {
|
|
48
|
+
return directRoute(input, "databricks-ai-gateway-codex", target.modelServiceName!);
|
|
54
49
|
}
|
|
55
50
|
if (target.capabilities.openResponses && supportsOpenResponses(target, features)) {
|
|
56
51
|
return directRoute(input, "databricks-open-responses", target.id);
|
|
@@ -164,8 +159,14 @@ function rejectStatefulResponses(features: RequestedFeatures): void {
|
|
|
164
159
|
if (unsupported.length > 0) throw new UnsupportedGatewayFeatureError(unsupported);
|
|
165
160
|
}
|
|
166
161
|
|
|
167
|
-
function
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
162
|
+
function canUseAiGatewayCodex(target: ModelTarget, features: RequestedFeatures): boolean {
|
|
163
|
+
return (
|
|
164
|
+
Boolean(target.capabilities.aiGatewayCodex && target.modelServiceName) &&
|
|
165
|
+
supportsDirectResponses(target, features)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Serving-endpoint ids stay on Databricks Responses; aliases and model-service slugs use Codex. */
|
|
170
|
+
function requestedServingEndpoint(requestedModel: string, target: ModelTarget): boolean {
|
|
171
|
+
return requestedModel.trim().replace(/^(?:dbx|databricks)\//i, "") === target.id;
|
|
171
172
|
}
|
package/src/transport.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { getExecutionContext } from "@databricks/appkit";
|
|
8
8
|
import { workspaceClient } from "@dbx-tools/databricks";
|
|
9
9
|
import { log } from "@dbx-tools/shared-core";
|
|
10
|
+
import { openaiChat } from "@dbx-tools/shared-model";
|
|
10
11
|
import { upstreamUrl, type GatewayRoute } from "@dbx-tools/shared-model-gateway";
|
|
11
12
|
|
|
12
13
|
const logger = log.logger("appkit/model-gateway/transport");
|
|
@@ -60,7 +61,7 @@ export async function fetchDatabricks(request: DatabricksTransportRequest): Prom
|
|
|
60
61
|
await client.config.authenticate(headers);
|
|
61
62
|
const url = upstreamUrl(host, request.route);
|
|
62
63
|
const startedAt = Date.now();
|
|
63
|
-
const body = JSON.stringify(
|
|
64
|
+
const body = JSON.stringify(directDatabricksRequestBody(request.route, request.body));
|
|
64
65
|
logger.debug("sending upstream request", {
|
|
65
66
|
model: request.route.upstreamModel,
|
|
66
67
|
path: new URL(url).pathname,
|
|
@@ -119,6 +120,20 @@ export async function fetchDatabricks(request: DatabricksTransportRequest): Prom
|
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
|
|
123
|
+
/** Remove client compatibility fields rejected by direct Databricks protocols. */
|
|
124
|
+
export function directDatabricksRequestBody(
|
|
125
|
+
route: Pick<GatewayRoute, "upstreamProtocol" | "upstreamModel">,
|
|
126
|
+
body: Readonly<Record<string, unknown>>,
|
|
127
|
+
): Record<string, unknown> {
|
|
128
|
+
const sanitized: Record<string, unknown> = { ...body, model: route.upstreamModel };
|
|
129
|
+
if (route.upstreamProtocol === "databricks-chat") {
|
|
130
|
+
openaiChat.stripUnsupportedChatFields(sanitized);
|
|
131
|
+
} else {
|
|
132
|
+
delete sanitized.parallel_tool_calls;
|
|
133
|
+
}
|
|
134
|
+
return sanitized;
|
|
135
|
+
}
|
|
136
|
+
|
|
122
137
|
/** Copy only response headers that belong to the public model protocol. */
|
|
123
138
|
export function gatewayResponseHeaders(upstream: Headers): Headers {
|
|
124
139
|
const headers = new Headers();
|