@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/src/registry.ts CHANGED
@@ -5,7 +5,13 @@
5
5
  */
6
6
 
7
7
  import { getExecutionContext, type WorkspaceClient } from "@databricks/appkit";
8
- import { metadata, modelCatalog, policy, resolve as modelResolve } from "@dbx-tools/model";
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: string, options?: ModelRegistryResolveOptions): Promise<ModelTarget | undefined>;
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: string;
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 registryContext();
68
+ const context = await this.context();
59
69
  logger.debug("loading catalogue", { host: context.host, ttlMs: this.ttlMs });
60
- const endpoints = await modelCatalog.listServingEndpoints(context.client, context.host, {
61
- cacheIdentity: context.identity,
62
- ttlMs: this.ttlMs,
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 = targets.find((target) =>
80
- target.aliases.some(
81
- (alias) => alias.localeCompare(requested, undefined, { sensitivity: "accent" }) === 0,
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 registryContext();
141
+ const context = await this.context();
128
142
  logger.debug("clearing catalogue", { host: context.host });
129
- await modelCatalog.clearServingEndpointsCache(context.host, context.identity);
130
- await modelCatalog.listServingEndpoints(context.client, context.host, {
131
- cacheIdentity: context.identity,
132
- ttlMs: this.ttlMs,
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
- targetsModelService(requestedModel, target) &&
40
- target.capabilities.aiGatewayCodex &&
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.capabilities.aiGatewayCodex &&
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 targetsModelService(requestedModel: string, target: ModelTarget): boolean {
168
- if (!target.modelServiceName) return false;
169
- const unqualified = requestedModel.trim().replace(/^(?:dbx|databricks)\//i, "");
170
- return unqualified === target.modelServiceName;
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({ ...request.body, model: request.route.upstreamModel });
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();