@dbx-tools/appkit-model-gateway 0.9.36

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 (44) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +63 -0
  3. package/index.ts +28 -0
  4. package/lib/index.d.ts +24 -0
  5. package/lib/index.js +21 -0
  6. package/lib/scripts/copy-manifest.d.ts +1 -0
  7. package/lib/scripts/copy-manifest.js +14 -0
  8. package/lib/src/gateway.d.ts +29 -0
  9. package/lib/src/gateway.js +127 -0
  10. package/lib/src/manifest.json +13 -0
  11. package/lib/src/models.d.ts +10 -0
  12. package/lib/src/models.js +119 -0
  13. package/lib/src/plugin.d.ts +32 -0
  14. package/lib/src/plugin.js +70 -0
  15. package/lib/src/protocols/decode.d.ts +17 -0
  16. package/lib/src/protocols/decode.js +262 -0
  17. package/lib/src/protocols/encode.d.ts +18 -0
  18. package/lib/src/protocols/encode.js +498 -0
  19. package/lib/src/reasoning-adapt.d.ts +27 -0
  20. package/lib/src/reasoning-adapt.js +61 -0
  21. package/lib/src/registry.d.ts +32 -0
  22. package/lib/src/registry.js +167 -0
  23. package/lib/src/router.d.ts +18 -0
  24. package/lib/src/router.js +139 -0
  25. package/lib/src/routes.d.ts +14 -0
  26. package/lib/src/routes.js +158 -0
  27. package/lib/src/translation.d.ts +8 -0
  28. package/lib/src/translation.js +156 -0
  29. package/lib/src/transport.d.ts +19 -0
  30. package/lib/src/transport.js +168 -0
  31. package/lib/tsconfig.tsbuildinfo +1 -0
  32. package/package.json +150 -0
  33. package/src/gateway.ts +167 -0
  34. package/src/manifest.json +13 -0
  35. package/src/models.ts +152 -0
  36. package/src/plugin.ts +95 -0
  37. package/src/protocols/decode.ts +281 -0
  38. package/src/protocols/encode.ts +515 -0
  39. package/src/reasoning-adapt.ts +87 -0
  40. package/src/registry.ts +225 -0
  41. package/src/router.ts +174 -0
  42. package/src/routes.ts +181 -0
  43. package/src/translation.ts +178 -0
  44. package/src/transport.ts +182 -0
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Vercel AI SDK fallback translation between external gateway protocols.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { createAnthropic } from "@ai-sdk/anthropic";
8
+ import { createOpenResponses } from "@ai-sdk/open-responses";
9
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
10
+ import { getExecutionContext } from "@databricks/appkit";
11
+ import { invoke, servingWire } from "@dbx-tools/model";
12
+ import { log } from "@dbx-tools/shared-core";
13
+ import type { GatewayRoute } from "@dbx-tools/shared-model-gateway";
14
+ import { streamText, type LanguageModel, type ToolSet } from "ai";
15
+
16
+ import { decodeGatewayRequest } from "./protocols/decode.ts";
17
+ import { encodeGatewayResponse, encodeGatewayStream } from "./protocols/encode.ts";
18
+
19
+ const logger = log.logger("appkit/model-gateway/translation");
20
+
21
+ /** Translate one request through a provider-owned AI SDK model. */
22
+ export async function translateGatewayRequest(
23
+ route: GatewayRoute,
24
+ body: Readonly<Record<string, unknown>>,
25
+ signal: AbortSignal,
26
+ ): Promise<Response> {
27
+ const decoded = decodeGatewayRequest(route.clientProtocol, body);
28
+ logger.debug("decoded request", {
29
+ clientProtocol: route.clientProtocol,
30
+ messageCount: decoded.messages.length,
31
+ model: route.target.id,
32
+ stream: body.stream === true,
33
+ toolCount: Object.keys(decoded.tools).length,
34
+ });
35
+ const model = await translationModel(route);
36
+ const result = streamText({
37
+ model,
38
+ messages: decoded.messages,
39
+ allowSystemInMessages: true,
40
+ ...(Object.keys(decoded.tools).length > 0 ? { tools: decoded.tools } : {}),
41
+ ...(decoded.maxOutputTokens !== undefined ? { maxOutputTokens: decoded.maxOutputTokens } : {}),
42
+ ...(decoded.temperature !== undefined ? { temperature: decoded.temperature } : {}),
43
+ ...(decoded.topP !== undefined ? { topP: decoded.topP } : {}),
44
+ maxRetries: 0,
45
+ abortSignal: signal,
46
+ onError: ({ error }) => {
47
+ logger.error("stream failed", { error, model: route.target.id });
48
+ },
49
+ onAbort: () => {
50
+ logger.debug("translation aborted", { model: route.target.id });
51
+ },
52
+ onFinish: ({ finishReason, usage }) => {
53
+ logger.debug("translation finished", {
54
+ finishReason,
55
+ inputTokens: usage.inputTokens,
56
+ model: route.target.id,
57
+ outputTokens: usage.outputTokens,
58
+ });
59
+ },
60
+ });
61
+
62
+ if (body.stream === true) {
63
+ logger.debug("returning translated stream", {
64
+ clientProtocol: route.clientProtocol,
65
+ model: route.target.id,
66
+ });
67
+ return new Response(
68
+ encodeGatewayStream(
69
+ route.clientProtocol,
70
+ route.target.id,
71
+ result.stream as AsyncIterable<import("ai").TextStreamPart<ToolSet>>,
72
+ ),
73
+ {
74
+ headers: {
75
+ "cache-control": "no-cache",
76
+ "content-type": "text/event-stream",
77
+ "x-accel-buffering": "no",
78
+ },
79
+ },
80
+ );
81
+ }
82
+
83
+ const [text, toolCalls, usage, finishReason] = await Promise.all([
84
+ result.text,
85
+ result.toolCalls,
86
+ result.usage,
87
+ result.finishReason,
88
+ ]);
89
+ logger.debug("returning translated response", {
90
+ clientProtocol: route.clientProtocol,
91
+ finishReason,
92
+ inputTokens: usage.inputTokens,
93
+ model: route.target.id,
94
+ outputTokens: usage.outputTokens,
95
+ toolCallCount: toolCalls.length,
96
+ });
97
+ return Response.json(
98
+ encodeGatewayResponse(route.clientProtocol, route.target.id, {
99
+ text,
100
+ toolCalls: toolCalls as import("ai").TypedToolCall<ToolSet>[],
101
+ usage,
102
+ finishReason,
103
+ }),
104
+ );
105
+ }
106
+
107
+ async function translationModel(route: GatewayRoute): Promise<LanguageModel> {
108
+ const client = getExecutionContext().client;
109
+ const host = (await client.config.getHost()).toString();
110
+ const headers = await invoke.authHeaders(client);
111
+ if (route.target.capabilities.responses) {
112
+ logger.debug("selected Responses provider", { model: route.target.id });
113
+ return createOpenResponses({
114
+ name: "databricks-responses",
115
+ url: invoke.responsesUrl(host),
116
+ headers,
117
+ })(route.target.id);
118
+ }
119
+ if (route.target.capabilities.anthropic) {
120
+ logger.debug("selected Anthropic provider", { model: route.target.id });
121
+ return createAnthropic({
122
+ name: "databricks-anthropic",
123
+ baseURL: new URL("serving-endpoints/anthropic/v1", host).toString(),
124
+ authToken: bearerToken(headers),
125
+ headers,
126
+ })(route.target.id);
127
+ }
128
+ logger.debug("selected Chat provider", { model: route.target.id });
129
+ return createOpenAICompatible({
130
+ name: "databricks-chat",
131
+ baseURL: new URL("serving-endpoints", host).toString(),
132
+ headers,
133
+ supportsStructuredOutputs: true,
134
+ fetch: compatibleFetch,
135
+ }).chatModel(route.target.id);
136
+ }
137
+
138
+ async function compatibleFetch(
139
+ input: Parameters<typeof fetch>[0],
140
+ init?: Parameters<typeof fetch>[1],
141
+ ): Promise<Response> {
142
+ const rewritten = await servingWire.rewriteServingRequest(input, init);
143
+ logger.debug("sending translated Chat request", {
144
+ rewritten: rewritten.input !== input || rewritten.init !== init,
145
+ });
146
+ const response = await fetch(rewritten.input, rewritten.init);
147
+ const responseDetails = {
148
+ contentType: response.headers.get("content-type"),
149
+ status: response.status,
150
+ };
151
+ if (response.ok) logger.debug("received translated Chat response", responseDetails);
152
+ else logger.warn("translated Chat request failed", responseDetails);
153
+ if (!response.body) return response;
154
+ const headers = new Headers(response.headers);
155
+ if (headers.get("content-type")?.includes("text/event-stream")) {
156
+ return new Response(servingWire.rewriteServingResponseStream(response.body), {
157
+ status: response.status,
158
+ statusText: response.statusText,
159
+ headers,
160
+ });
161
+ }
162
+ const body = servingWire.rewriteServingResponseBody(await response.text());
163
+ headers.delete("content-length");
164
+ return new Response(body, {
165
+ status: response.status,
166
+ statusText: response.statusText,
167
+ headers,
168
+ });
169
+ }
170
+
171
+ function bearerToken(headers: Readonly<Record<string, string>>): string {
172
+ const authorization = headers.authorization ?? headers.Authorization;
173
+ const [scheme, token] = authorization?.split(/\s+/, 2) ?? [];
174
+ if (scheme?.toLowerCase() !== "bearer" || !token) {
175
+ throw new Error("Databricks authentication did not produce a bearer token");
176
+ }
177
+ return token;
178
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Authenticated streaming transport for direct Databricks gateway routes.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { getExecutionContext } from "@databricks/appkit";
8
+ import { workspaceClient } from "@dbx-tools/databricks";
9
+ import { invoke } from "@dbx-tools/model";
10
+ import { log } from "@dbx-tools/shared-core";
11
+ import type { GatewayRoute } from "@dbx-tools/shared-model-gateway";
12
+
13
+ const logger = log.logger("appkit/model-gateway/transport");
14
+ const ERROR_PREVIEW_BYTES = 8 * 1024;
15
+
16
+ const BLOCKED_REQUEST_HEADERS = new Set([
17
+ "accept-encoding",
18
+ "authorization",
19
+ "connection",
20
+ "content-length",
21
+ "cookie",
22
+ "host",
23
+ "proxy-authorization",
24
+ "transfer-encoding",
25
+ "x-api-key",
26
+ ]);
27
+
28
+ const BLOCKED_RESPONSE_HEADERS = new Set([
29
+ "connection",
30
+ "content-encoding",
31
+ "content-length",
32
+ "keep-alive",
33
+ "proxy-authenticate",
34
+ "proxy-authorization",
35
+ "set-cookie",
36
+ "te",
37
+ "trailer",
38
+ "transfer-encoding",
39
+ "upgrade",
40
+ ]);
41
+
42
+ /** Inputs for one direct Databricks protocol request. */
43
+ export interface DatabricksTransportRequest {
44
+ readonly route: GatewayRoute;
45
+ readonly body: Readonly<Record<string, unknown>>;
46
+ readonly headers: Headers;
47
+ readonly signal: AbortSignal;
48
+ }
49
+
50
+ /** Execute a direct route with SDK-owned authentication and an unbuffered response body. */
51
+ export async function fetchDatabricks(request: DatabricksTransportRequest): Promise<Response> {
52
+ const client = getExecutionContext().client;
53
+ const host = (await client.config.getHost()).toString();
54
+ const headers = new Headers({ "content-type": "application/json" });
55
+ request.headers.forEach((value, name) => {
56
+ const normalized = name.toLowerCase();
57
+ if (!BLOCKED_REQUEST_HEADERS.has(normalized) && !normalized.startsWith("x-forwarded-")) {
58
+ headers.set(name, value);
59
+ }
60
+ });
61
+ await client.config.authenticate(headers);
62
+ const url = upstreamUrl(host, request.route);
63
+ const startedAt = Date.now();
64
+ const body = JSON.stringify({ ...request.body, model: request.route.upstreamModel });
65
+ logger.debug("sending upstream request", {
66
+ model: request.route.upstreamModel,
67
+ path: new URL(url).pathname,
68
+ stream: request.body.stream === true,
69
+ upstreamProtocol: request.route.upstreamProtocol,
70
+ });
71
+ try {
72
+ let response = await fetch(url, {
73
+ method: "POST",
74
+ headers,
75
+ body,
76
+ signal: request.signal,
77
+ });
78
+ if (
79
+ response.status === 401 &&
80
+ (await workspaceClient.refreshWorkspaceClientAuthentication(client))
81
+ ) {
82
+ logger.debug("retrying rejected upstream credential", {
83
+ model: request.route.upstreamModel,
84
+ upstreamProtocol: request.route.upstreamProtocol,
85
+ });
86
+ await response.body?.cancel();
87
+ headers.delete("authorization");
88
+ headers.delete("x-databricks-workspace-id");
89
+ await client.config.authenticate(headers);
90
+ response = await fetch(url, {
91
+ method: "POST",
92
+ headers,
93
+ body,
94
+ signal: request.signal,
95
+ });
96
+ }
97
+ const details = {
98
+ elapsedMs: Date.now() - startedAt,
99
+ model: request.route.upstreamModel,
100
+ requestId:
101
+ response.headers.get("x-request-id") ?? response.headers.get("x-databricks-request-id"),
102
+ status: response.status,
103
+ upstreamProtocol: request.route.upstreamProtocol,
104
+ };
105
+ if (response.ok) {
106
+ logger.debug("received upstream response", details);
107
+ } else {
108
+ logger.warn("upstream response failed", details);
109
+ void logUpstreamFailureBody(response.clone(), details);
110
+ }
111
+ return response;
112
+ } catch (error) {
113
+ logger.error("upstream fetch failed", {
114
+ elapsedMs: Date.now() - startedAt,
115
+ error,
116
+ model: request.route.upstreamModel,
117
+ upstreamProtocol: request.route.upstreamProtocol,
118
+ });
119
+ throw error;
120
+ }
121
+ }
122
+
123
+ /** Copy only response headers that belong to the public model protocol. */
124
+ export function gatewayResponseHeaders(upstream: Headers): Headers {
125
+ const headers = new Headers();
126
+ upstream.forEach((value, name) => {
127
+ if (!BLOCKED_RESPONSE_HEADERS.has(name.toLowerCase())) headers.set(name, value);
128
+ });
129
+ return headers;
130
+ }
131
+
132
+ /** Resolve the exact upstream URL for a deterministic direct route. */
133
+ export function upstreamUrl(host: string, route: GatewayRoute): string {
134
+ switch (route.upstreamProtocol) {
135
+ case "databricks-ai-gateway-codex":
136
+ return invoke.aiGatewayCodexResponsesUrl(host);
137
+ case "databricks-responses":
138
+ return invoke.responsesUrl(host);
139
+ case "databricks-open-responses":
140
+ return invoke.openResponsesUrl(host);
141
+ case "databricks-chat":
142
+ return invoke.chatCompletionsUrl(host);
143
+ case "databricks-anthropic":
144
+ return invoke.anthropicMessagesUrl(host);
145
+ case "databricks-embeddings":
146
+ return invoke.invocationsUrl(host, route.upstreamModel);
147
+ case "ai-sdk":
148
+ throw new Error("AI SDK routes do not use the direct Databricks transport");
149
+ }
150
+ }
151
+
152
+ async function logUpstreamFailureBody(
153
+ response: Response,
154
+ details: Record<string, unknown>,
155
+ ): Promise<void> {
156
+ const reader = response.body?.getReader();
157
+ if (!reader) return;
158
+ const decoder = new TextDecoder();
159
+ let preview = "";
160
+ let bytes = 0;
161
+ try {
162
+ while (bytes < ERROR_PREVIEW_BYTES) {
163
+ const chunk = await reader.read();
164
+ if (chunk.done) break;
165
+ const remaining = ERROR_PREVIEW_BYTES - bytes;
166
+ const value = chunk.value.subarray(0, remaining);
167
+ bytes += value.byteLength;
168
+ preview += decoder.decode(value, { stream: true });
169
+ if (value.byteLength < chunk.value.byteLength) break;
170
+ }
171
+ preview += decoder.decode();
172
+ logger.warn("upstream error body", {
173
+ ...details,
174
+ preview,
175
+ truncated: bytes >= ERROR_PREVIEW_BYTES,
176
+ });
177
+ } catch (error) {
178
+ logger.debug("could not read upstream error body", { ...details, error });
179
+ } finally {
180
+ await reader.cancel().catch(() => undefined);
181
+ }
182
+ }