@fabricorg/experiments-evals 0.1.0

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 ADDED
@@ -0,0 +1,15 @@
1
+ # @fabricorg/experiments-evals
2
+
3
+ Provider-neutral TypeScript evaluation contracts, built-in LLM judges, code
4
+ evaluators, bounded-concurrency execution, hard judge-token reservations, and
5
+ score aggregation for Fabric Experiments.
6
+
7
+ ```ts
8
+ import { executeEvalRun, relevanceJudge } from '@fabricorg/experiments-evals';
9
+ import { DatabricksModelClient } from '@fabricorg/experiments-evals/databricks';
10
+ ```
11
+
12
+ Databricks is an explicit adapter subpath; it is not exported from the portable
13
+ package root.
14
+
15
+ See [experiments.fabric.pro/docs/ai-quality/overview](https://experiments.fabric.pro/docs/ai-quality/overview).
@@ -0,0 +1,120 @@
1
+ 'use strict';
2
+
3
+ // src/databricks-model-client.ts
4
+ var DatabricksModelClient = class {
5
+ endpoint;
6
+ /** Exposed for wiring assertions (deps-construction tests). */
7
+ authMode;
8
+ host;
9
+ auth;
10
+ fetchImpl;
11
+ timeoutMs;
12
+ oauthToken = null;
13
+ constructor(config) {
14
+ this.host = normalizeHost(config.host);
15
+ this.endpoint = config.endpoint;
16
+ this.auth = config.auth;
17
+ this.authMode = config.auth.kind;
18
+ this.fetchImpl = config.fetchImpl ?? fetch;
19
+ this.timeoutMs = config.timeoutMs ?? 6e4;
20
+ if (!Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0) {
21
+ throw new Error("Databricks Model Serving timeoutMs must be positive");
22
+ }
23
+ }
24
+ async complete(messages, options) {
25
+ if (!Number.isInteger(options.maxOutputTokens) || options.maxOutputTokens <= 0) {
26
+ throw new Error("maxOutputTokens must be a positive integer");
27
+ }
28
+ if (!Number.isFinite(options.temperature) || options.temperature < 0) {
29
+ throw new Error("temperature must be a non-negative finite number");
30
+ }
31
+ const response = await this.fetchImpl(
32
+ `${this.host}/serving-endpoints/${encodeURIComponent(this.endpoint)}/invocations`,
33
+ {
34
+ method: "POST",
35
+ headers: {
36
+ authorization: `Bearer ${await this.resolveToken()}`,
37
+ "content-type": "application/json"
38
+ },
39
+ body: JSON.stringify({
40
+ messages: messages.map((m) => ({ role: m.role, content: m.content })),
41
+ temperature: options.temperature,
42
+ max_tokens: options.maxOutputTokens
43
+ }),
44
+ signal: AbortSignal.timeout(this.timeoutMs)
45
+ }
46
+ );
47
+ if (!response.ok) {
48
+ const body = await response.text();
49
+ throw new Error(
50
+ `Model Serving invocation failed (${response.status}): ${body.slice(0, 500)}`
51
+ );
52
+ }
53
+ const payload = await response.json();
54
+ const parsed = parseServingResponse(payload);
55
+ return {
56
+ text: parsed.text,
57
+ tokens: {
58
+ inputTokens: parsed.inputTokens,
59
+ outputTokens: parsed.outputTokens
60
+ }
61
+ };
62
+ }
63
+ async resolveToken() {
64
+ if (this.auth.kind === "pat") return this.auth.token;
65
+ if (this.oauthToken && this.oauthToken.expiresAt > Date.now() + 6e4) {
66
+ return this.oauthToken.value;
67
+ }
68
+ const response = await this.fetchImpl(`${this.host}/oidc/v1/token`, {
69
+ method: "POST",
70
+ headers: {
71
+ authorization: `Basic ${toBase64(`${this.auth.clientId}:${this.auth.clientSecret}`)}`,
72
+ "content-type": "application/x-www-form-urlencoded"
73
+ },
74
+ body: new URLSearchParams({ grant_type: "client_credentials", scope: "all-apis" }),
75
+ signal: AbortSignal.timeout(this.timeoutMs)
76
+ });
77
+ if (!response.ok) throw new Error(`Databricks OAuth exchange failed (${response.status})`);
78
+ const body = await response.json();
79
+ if (!body.access_token) throw new Error("Databricks OAuth exchange returned no access token");
80
+ this.oauthToken = {
81
+ value: body.access_token,
82
+ expiresAt: Date.now() + (body.expires_in ?? 3600) * 1e3
83
+ };
84
+ return this.oauthToken.value;
85
+ }
86
+ };
87
+ function parseServingResponse(payload) {
88
+ if (!isRecord(payload)) throw new Error("Model Serving returned an invalid response object");
89
+ const choices = payload.choices;
90
+ const first = Array.isArray(choices) ? choices[0] : void 0;
91
+ const message = isRecord(first) && isRecord(first.message) ? first.message : void 0;
92
+ const text = message?.content;
93
+ if (typeof text !== "string" || text.trim() === "") {
94
+ throw new Error("Model Serving returned no completion text");
95
+ }
96
+ const usage = isRecord(payload.usage) ? payload.usage : void 0;
97
+ const inputTokens = usage?.prompt_tokens;
98
+ const outputTokens = usage?.completion_tokens;
99
+ if (!isTokenCount(inputTokens) || !isTokenCount(outputTokens)) {
100
+ throw new Error("Model Serving returned invalid token usage");
101
+ }
102
+ return { text, inputTokens, outputTokens };
103
+ }
104
+ function isRecord(value) {
105
+ return value !== null && typeof value === "object" && !Array.isArray(value);
106
+ }
107
+ function isTokenCount(value) {
108
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
109
+ }
110
+ function normalizeHost(host) {
111
+ return host.startsWith("http://") || host.startsWith("https://") ? host.replace(/\/$/, "") : `https://${host.replace(/\/$/, "")}`;
112
+ }
113
+ function toBase64(value) {
114
+ if (typeof Buffer !== "undefined") return Buffer.from(value).toString("base64");
115
+ return btoa(value);
116
+ }
117
+
118
+ exports.DatabricksModelClient = DatabricksModelClient;
119
+ //# sourceMappingURL=databricks.cjs.map
120
+ //# sourceMappingURL=databricks.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/databricks-model-client.ts"],"names":[],"mappings":"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","file":"databricks.cjs","sourcesContent":["import type {\n ChatMessage,\n ModelClient,\n ModelCompletionOptions,\n ModelResponse,\n} from './model-client.js';\n\n/**\n * Transport-agnostic Databricks Model Serving chat client\n * (`/serving-endpoints/{name}/invocations`, OpenAI-compatible chat payload).\n *\n * Auth mirrors the warehouse wiring: a PAT (`token`) wins when provided,\n * otherwise M2M OAuth client credentials are exchanged at\n * `/oidc/v1/token` and cached per instance until shortly before expiry.\n *\n * The `fetchImpl` injection keeps this package free of HTTP-client deps and\n * lets tests assert the exact request shape. Both the control plane\n * (`apps/api/src/server/model-client.ts`) and the Temporal worker\n * (`agents/harness-deploy`) wrap this class with their own env resolution.\n */\n\nexport type DatabricksAuth =\n | { kind: 'pat'; token: string }\n | { kind: 'oauth-m2m'; clientId: string; clientSecret: string };\n\nexport interface DatabricksModelClientConfig {\n /** Workspace host — bare (`dbc-x.cloud.databricks.com`) or with scheme. */\n host: string;\n /** Serving endpoint name; recorded in EvalScore.modelEndpoint. */\n endpoint: string;\n auth: DatabricksAuth;\n /** Injected for tests; defaults to global fetch. */\n fetchImpl?: typeof fetch;\n /** Abort Model Serving and OAuth calls after this duration. Default: 60s. */\n timeoutMs?: number;\n}\n\nexport class DatabricksModelClient implements ModelClient {\n readonly endpoint: string;\n /** Exposed for wiring assertions (deps-construction tests). */\n readonly authMode: DatabricksAuth['kind'];\n private readonly host: string;\n private readonly auth: DatabricksAuth;\n private readonly fetchImpl: typeof fetch;\n private readonly timeoutMs: number;\n private oauthToken: { value: string; expiresAt: number } | null = null;\n\n constructor(config: DatabricksModelClientConfig) {\n this.host = normalizeHost(config.host);\n this.endpoint = config.endpoint;\n this.auth = config.auth;\n this.authMode = config.auth.kind;\n this.fetchImpl = config.fetchImpl ?? fetch;\n this.timeoutMs = config.timeoutMs ?? 60_000;\n if (!Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0) {\n throw new Error('Databricks Model Serving timeoutMs must be positive');\n }\n }\n\n async complete(\n messages: readonly ChatMessage[],\n options: ModelCompletionOptions,\n ): Promise<ModelResponse> {\n if (!Number.isInteger(options.maxOutputTokens) || options.maxOutputTokens <= 0) {\n throw new Error('maxOutputTokens must be a positive integer');\n }\n if (!Number.isFinite(options.temperature) || options.temperature < 0) {\n throw new Error('temperature must be a non-negative finite number');\n }\n const response = await this.fetchImpl(\n `${this.host}/serving-endpoints/${encodeURIComponent(this.endpoint)}/invocations`,\n {\n method: 'POST',\n headers: {\n authorization: `Bearer ${await this.resolveToken()}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify({\n messages: messages.map((m) => ({ role: m.role, content: m.content })),\n temperature: options.temperature,\n max_tokens: options.maxOutputTokens,\n }),\n signal: AbortSignal.timeout(this.timeoutMs),\n },\n );\n if (!response.ok) {\n const body = await response.text();\n throw new Error(\n `Model Serving invocation failed (${response.status}): ${body.slice(0, 500)}`,\n );\n }\n const payload = (await response.json()) as unknown;\n const parsed = parseServingResponse(payload);\n return {\n text: parsed.text,\n tokens: {\n inputTokens: parsed.inputTokens,\n outputTokens: parsed.outputTokens,\n },\n };\n }\n\n private async resolveToken(): Promise<string> {\n if (this.auth.kind === 'pat') return this.auth.token;\n if (this.oauthToken && this.oauthToken.expiresAt > Date.now() + 60_000) {\n return this.oauthToken.value;\n }\n const response = await this.fetchImpl(`${this.host}/oidc/v1/token`, {\n method: 'POST',\n headers: {\n authorization: `Basic ${toBase64(`${this.auth.clientId}:${this.auth.clientSecret}`)}`,\n 'content-type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({ grant_type: 'client_credentials', scope: 'all-apis' }),\n signal: AbortSignal.timeout(this.timeoutMs),\n });\n if (!response.ok) throw new Error(`Databricks OAuth exchange failed (${response.status})`);\n const body = (await response.json()) as { access_token?: string; expires_in?: number };\n if (!body.access_token) throw new Error('Databricks OAuth exchange returned no access token');\n this.oauthToken = {\n value: body.access_token,\n expiresAt: Date.now() + (body.expires_in ?? 3600) * 1000,\n };\n return this.oauthToken.value;\n }\n}\n\nfunction parseServingResponse(payload: unknown): {\n text: string;\n inputTokens: number;\n outputTokens: number;\n} {\n if (!isRecord(payload)) throw new Error('Model Serving returned an invalid response object');\n const choices = payload.choices;\n const first = Array.isArray(choices) ? choices[0] : undefined;\n const message = isRecord(first) && isRecord(first.message) ? first.message : undefined;\n const text = message?.content;\n if (typeof text !== 'string' || text.trim() === '') {\n throw new Error('Model Serving returned no completion text');\n }\n const usage = isRecord(payload.usage) ? payload.usage : undefined;\n const inputTokens = usage?.prompt_tokens;\n const outputTokens = usage?.completion_tokens;\n if (!isTokenCount(inputTokens) || !isTokenCount(outputTokens)) {\n throw new Error('Model Serving returned invalid token usage');\n }\n return { text, inputTokens, outputTokens };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction isTokenCount(value: unknown): value is number {\n return typeof value === 'number' && Number.isInteger(value) && value >= 0;\n}\n\nfunction normalizeHost(host: string): string {\n return host.startsWith('http://') || host.startsWith('https://')\n ? host.replace(/\\/$/, '')\n : `https://${host.replace(/\\/$/, '')}`;\n}\n\n/** Node has Buffer; edge/browser runtimes have btoa. Credentials are ASCII. */\nfunction toBase64(value: string): string {\n if (typeof Buffer !== 'undefined') return Buffer.from(value).toString('base64');\n return btoa(value);\n}\n"]}
@@ -0,0 +1,50 @@
1
+ import { M as ModelClient, C as ChatMessage, g as ModelCompletionOptions, h as ModelResponse } from './model-client-B7yk_i1s.cjs';
2
+ import 'zod';
3
+
4
+ /**
5
+ * Transport-agnostic Databricks Model Serving chat client
6
+ * (`/serving-endpoints/{name}/invocations`, OpenAI-compatible chat payload).
7
+ *
8
+ * Auth mirrors the warehouse wiring: a PAT (`token`) wins when provided,
9
+ * otherwise M2M OAuth client credentials are exchanged at
10
+ * `/oidc/v1/token` and cached per instance until shortly before expiry.
11
+ *
12
+ * The `fetchImpl` injection keeps this package free of HTTP-client deps and
13
+ * lets tests assert the exact request shape. Both the control plane
14
+ * (`apps/api/src/server/model-client.ts`) and the Temporal worker
15
+ * (`agents/harness-deploy`) wrap this class with their own env resolution.
16
+ */
17
+ type DatabricksAuth = {
18
+ kind: 'pat';
19
+ token: string;
20
+ } | {
21
+ kind: 'oauth-m2m';
22
+ clientId: string;
23
+ clientSecret: string;
24
+ };
25
+ interface DatabricksModelClientConfig {
26
+ /** Workspace host — bare (`dbc-x.cloud.databricks.com`) or with scheme. */
27
+ host: string;
28
+ /** Serving endpoint name; recorded in EvalScore.modelEndpoint. */
29
+ endpoint: string;
30
+ auth: DatabricksAuth;
31
+ /** Injected for tests; defaults to global fetch. */
32
+ fetchImpl?: typeof fetch;
33
+ /** Abort Model Serving and OAuth calls after this duration. Default: 60s. */
34
+ timeoutMs?: number;
35
+ }
36
+ declare class DatabricksModelClient implements ModelClient {
37
+ readonly endpoint: string;
38
+ /** Exposed for wiring assertions (deps-construction tests). */
39
+ readonly authMode: DatabricksAuth['kind'];
40
+ private readonly host;
41
+ private readonly auth;
42
+ private readonly fetchImpl;
43
+ private readonly timeoutMs;
44
+ private oauthToken;
45
+ constructor(config: DatabricksModelClientConfig);
46
+ complete(messages: readonly ChatMessage[], options: ModelCompletionOptions): Promise<ModelResponse>;
47
+ private resolveToken;
48
+ }
49
+
50
+ export { type DatabricksAuth, DatabricksModelClient, type DatabricksModelClientConfig };
@@ -0,0 +1,50 @@
1
+ import { M as ModelClient, C as ChatMessage, g as ModelCompletionOptions, h as ModelResponse } from './model-client-B7yk_i1s.js';
2
+ import 'zod';
3
+
4
+ /**
5
+ * Transport-agnostic Databricks Model Serving chat client
6
+ * (`/serving-endpoints/{name}/invocations`, OpenAI-compatible chat payload).
7
+ *
8
+ * Auth mirrors the warehouse wiring: a PAT (`token`) wins when provided,
9
+ * otherwise M2M OAuth client credentials are exchanged at
10
+ * `/oidc/v1/token` and cached per instance until shortly before expiry.
11
+ *
12
+ * The `fetchImpl` injection keeps this package free of HTTP-client deps and
13
+ * lets tests assert the exact request shape. Both the control plane
14
+ * (`apps/api/src/server/model-client.ts`) and the Temporal worker
15
+ * (`agents/harness-deploy`) wrap this class with their own env resolution.
16
+ */
17
+ type DatabricksAuth = {
18
+ kind: 'pat';
19
+ token: string;
20
+ } | {
21
+ kind: 'oauth-m2m';
22
+ clientId: string;
23
+ clientSecret: string;
24
+ };
25
+ interface DatabricksModelClientConfig {
26
+ /** Workspace host — bare (`dbc-x.cloud.databricks.com`) or with scheme. */
27
+ host: string;
28
+ /** Serving endpoint name; recorded in EvalScore.modelEndpoint. */
29
+ endpoint: string;
30
+ auth: DatabricksAuth;
31
+ /** Injected for tests; defaults to global fetch. */
32
+ fetchImpl?: typeof fetch;
33
+ /** Abort Model Serving and OAuth calls after this duration. Default: 60s. */
34
+ timeoutMs?: number;
35
+ }
36
+ declare class DatabricksModelClient implements ModelClient {
37
+ readonly endpoint: string;
38
+ /** Exposed for wiring assertions (deps-construction tests). */
39
+ readonly authMode: DatabricksAuth['kind'];
40
+ private readonly host;
41
+ private readonly auth;
42
+ private readonly fetchImpl;
43
+ private readonly timeoutMs;
44
+ private oauthToken;
45
+ constructor(config: DatabricksModelClientConfig);
46
+ complete(messages: readonly ChatMessage[], options: ModelCompletionOptions): Promise<ModelResponse>;
47
+ private resolveToken;
48
+ }
49
+
50
+ export { type DatabricksAuth, DatabricksModelClient, type DatabricksModelClientConfig };
@@ -0,0 +1,118 @@
1
+ // src/databricks-model-client.ts
2
+ var DatabricksModelClient = class {
3
+ endpoint;
4
+ /** Exposed for wiring assertions (deps-construction tests). */
5
+ authMode;
6
+ host;
7
+ auth;
8
+ fetchImpl;
9
+ timeoutMs;
10
+ oauthToken = null;
11
+ constructor(config) {
12
+ this.host = normalizeHost(config.host);
13
+ this.endpoint = config.endpoint;
14
+ this.auth = config.auth;
15
+ this.authMode = config.auth.kind;
16
+ this.fetchImpl = config.fetchImpl ?? fetch;
17
+ this.timeoutMs = config.timeoutMs ?? 6e4;
18
+ if (!Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0) {
19
+ throw new Error("Databricks Model Serving timeoutMs must be positive");
20
+ }
21
+ }
22
+ async complete(messages, options) {
23
+ if (!Number.isInteger(options.maxOutputTokens) || options.maxOutputTokens <= 0) {
24
+ throw new Error("maxOutputTokens must be a positive integer");
25
+ }
26
+ if (!Number.isFinite(options.temperature) || options.temperature < 0) {
27
+ throw new Error("temperature must be a non-negative finite number");
28
+ }
29
+ const response = await this.fetchImpl(
30
+ `${this.host}/serving-endpoints/${encodeURIComponent(this.endpoint)}/invocations`,
31
+ {
32
+ method: "POST",
33
+ headers: {
34
+ authorization: `Bearer ${await this.resolveToken()}`,
35
+ "content-type": "application/json"
36
+ },
37
+ body: JSON.stringify({
38
+ messages: messages.map((m) => ({ role: m.role, content: m.content })),
39
+ temperature: options.temperature,
40
+ max_tokens: options.maxOutputTokens
41
+ }),
42
+ signal: AbortSignal.timeout(this.timeoutMs)
43
+ }
44
+ );
45
+ if (!response.ok) {
46
+ const body = await response.text();
47
+ throw new Error(
48
+ `Model Serving invocation failed (${response.status}): ${body.slice(0, 500)}`
49
+ );
50
+ }
51
+ const payload = await response.json();
52
+ const parsed = parseServingResponse(payload);
53
+ return {
54
+ text: parsed.text,
55
+ tokens: {
56
+ inputTokens: parsed.inputTokens,
57
+ outputTokens: parsed.outputTokens
58
+ }
59
+ };
60
+ }
61
+ async resolveToken() {
62
+ if (this.auth.kind === "pat") return this.auth.token;
63
+ if (this.oauthToken && this.oauthToken.expiresAt > Date.now() + 6e4) {
64
+ return this.oauthToken.value;
65
+ }
66
+ const response = await this.fetchImpl(`${this.host}/oidc/v1/token`, {
67
+ method: "POST",
68
+ headers: {
69
+ authorization: `Basic ${toBase64(`${this.auth.clientId}:${this.auth.clientSecret}`)}`,
70
+ "content-type": "application/x-www-form-urlencoded"
71
+ },
72
+ body: new URLSearchParams({ grant_type: "client_credentials", scope: "all-apis" }),
73
+ signal: AbortSignal.timeout(this.timeoutMs)
74
+ });
75
+ if (!response.ok) throw new Error(`Databricks OAuth exchange failed (${response.status})`);
76
+ const body = await response.json();
77
+ if (!body.access_token) throw new Error("Databricks OAuth exchange returned no access token");
78
+ this.oauthToken = {
79
+ value: body.access_token,
80
+ expiresAt: Date.now() + (body.expires_in ?? 3600) * 1e3
81
+ };
82
+ return this.oauthToken.value;
83
+ }
84
+ };
85
+ function parseServingResponse(payload) {
86
+ if (!isRecord(payload)) throw new Error("Model Serving returned an invalid response object");
87
+ const choices = payload.choices;
88
+ const first = Array.isArray(choices) ? choices[0] : void 0;
89
+ const message = isRecord(first) && isRecord(first.message) ? first.message : void 0;
90
+ const text = message?.content;
91
+ if (typeof text !== "string" || text.trim() === "") {
92
+ throw new Error("Model Serving returned no completion text");
93
+ }
94
+ const usage = isRecord(payload.usage) ? payload.usage : void 0;
95
+ const inputTokens = usage?.prompt_tokens;
96
+ const outputTokens = usage?.completion_tokens;
97
+ if (!isTokenCount(inputTokens) || !isTokenCount(outputTokens)) {
98
+ throw new Error("Model Serving returned invalid token usage");
99
+ }
100
+ return { text, inputTokens, outputTokens };
101
+ }
102
+ function isRecord(value) {
103
+ return value !== null && typeof value === "object" && !Array.isArray(value);
104
+ }
105
+ function isTokenCount(value) {
106
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
107
+ }
108
+ function normalizeHost(host) {
109
+ return host.startsWith("http://") || host.startsWith("https://") ? host.replace(/\/$/, "") : `https://${host.replace(/\/$/, "")}`;
110
+ }
111
+ function toBase64(value) {
112
+ if (typeof Buffer !== "undefined") return Buffer.from(value).toString("base64");
113
+ return btoa(value);
114
+ }
115
+
116
+ export { DatabricksModelClient };
117
+ //# sourceMappingURL=databricks.js.map
118
+ //# sourceMappingURL=databricks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/databricks-model-client.ts"],"names":[],"mappings":"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","file":"databricks.js","sourcesContent":["import type {\n ChatMessage,\n ModelClient,\n ModelCompletionOptions,\n ModelResponse,\n} from './model-client.js';\n\n/**\n * Transport-agnostic Databricks Model Serving chat client\n * (`/serving-endpoints/{name}/invocations`, OpenAI-compatible chat payload).\n *\n * Auth mirrors the warehouse wiring: a PAT (`token`) wins when provided,\n * otherwise M2M OAuth client credentials are exchanged at\n * `/oidc/v1/token` and cached per instance until shortly before expiry.\n *\n * The `fetchImpl` injection keeps this package free of HTTP-client deps and\n * lets tests assert the exact request shape. Both the control plane\n * (`apps/api/src/server/model-client.ts`) and the Temporal worker\n * (`agents/harness-deploy`) wrap this class with their own env resolution.\n */\n\nexport type DatabricksAuth =\n | { kind: 'pat'; token: string }\n | { kind: 'oauth-m2m'; clientId: string; clientSecret: string };\n\nexport interface DatabricksModelClientConfig {\n /** Workspace host — bare (`dbc-x.cloud.databricks.com`) or with scheme. */\n host: string;\n /** Serving endpoint name; recorded in EvalScore.modelEndpoint. */\n endpoint: string;\n auth: DatabricksAuth;\n /** Injected for tests; defaults to global fetch. */\n fetchImpl?: typeof fetch;\n /** Abort Model Serving and OAuth calls after this duration. Default: 60s. */\n timeoutMs?: number;\n}\n\nexport class DatabricksModelClient implements ModelClient {\n readonly endpoint: string;\n /** Exposed for wiring assertions (deps-construction tests). */\n readonly authMode: DatabricksAuth['kind'];\n private readonly host: string;\n private readonly auth: DatabricksAuth;\n private readonly fetchImpl: typeof fetch;\n private readonly timeoutMs: number;\n private oauthToken: { value: string; expiresAt: number } | null = null;\n\n constructor(config: DatabricksModelClientConfig) {\n this.host = normalizeHost(config.host);\n this.endpoint = config.endpoint;\n this.auth = config.auth;\n this.authMode = config.auth.kind;\n this.fetchImpl = config.fetchImpl ?? fetch;\n this.timeoutMs = config.timeoutMs ?? 60_000;\n if (!Number.isFinite(this.timeoutMs) || this.timeoutMs <= 0) {\n throw new Error('Databricks Model Serving timeoutMs must be positive');\n }\n }\n\n async complete(\n messages: readonly ChatMessage[],\n options: ModelCompletionOptions,\n ): Promise<ModelResponse> {\n if (!Number.isInteger(options.maxOutputTokens) || options.maxOutputTokens <= 0) {\n throw new Error('maxOutputTokens must be a positive integer');\n }\n if (!Number.isFinite(options.temperature) || options.temperature < 0) {\n throw new Error('temperature must be a non-negative finite number');\n }\n const response = await this.fetchImpl(\n `${this.host}/serving-endpoints/${encodeURIComponent(this.endpoint)}/invocations`,\n {\n method: 'POST',\n headers: {\n authorization: `Bearer ${await this.resolveToken()}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify({\n messages: messages.map((m) => ({ role: m.role, content: m.content })),\n temperature: options.temperature,\n max_tokens: options.maxOutputTokens,\n }),\n signal: AbortSignal.timeout(this.timeoutMs),\n },\n );\n if (!response.ok) {\n const body = await response.text();\n throw new Error(\n `Model Serving invocation failed (${response.status}): ${body.slice(0, 500)}`,\n );\n }\n const payload = (await response.json()) as unknown;\n const parsed = parseServingResponse(payload);\n return {\n text: parsed.text,\n tokens: {\n inputTokens: parsed.inputTokens,\n outputTokens: parsed.outputTokens,\n },\n };\n }\n\n private async resolveToken(): Promise<string> {\n if (this.auth.kind === 'pat') return this.auth.token;\n if (this.oauthToken && this.oauthToken.expiresAt > Date.now() + 60_000) {\n return this.oauthToken.value;\n }\n const response = await this.fetchImpl(`${this.host}/oidc/v1/token`, {\n method: 'POST',\n headers: {\n authorization: `Basic ${toBase64(`${this.auth.clientId}:${this.auth.clientSecret}`)}`,\n 'content-type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({ grant_type: 'client_credentials', scope: 'all-apis' }),\n signal: AbortSignal.timeout(this.timeoutMs),\n });\n if (!response.ok) throw new Error(`Databricks OAuth exchange failed (${response.status})`);\n const body = (await response.json()) as { access_token?: string; expires_in?: number };\n if (!body.access_token) throw new Error('Databricks OAuth exchange returned no access token');\n this.oauthToken = {\n value: body.access_token,\n expiresAt: Date.now() + (body.expires_in ?? 3600) * 1000,\n };\n return this.oauthToken.value;\n }\n}\n\nfunction parseServingResponse(payload: unknown): {\n text: string;\n inputTokens: number;\n outputTokens: number;\n} {\n if (!isRecord(payload)) throw new Error('Model Serving returned an invalid response object');\n const choices = payload.choices;\n const first = Array.isArray(choices) ? choices[0] : undefined;\n const message = isRecord(first) && isRecord(first.message) ? first.message : undefined;\n const text = message?.content;\n if (typeof text !== 'string' || text.trim() === '') {\n throw new Error('Model Serving returned no completion text');\n }\n const usage = isRecord(payload.usage) ? payload.usage : undefined;\n const inputTokens = usage?.prompt_tokens;\n const outputTokens = usage?.completion_tokens;\n if (!isTokenCount(inputTokens) || !isTokenCount(outputTokens)) {\n throw new Error('Model Serving returned invalid token usage');\n }\n return { text, inputTokens, outputTokens };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === 'object' && !Array.isArray(value);\n}\n\nfunction isTokenCount(value: unknown): value is number {\n return typeof value === 'number' && Number.isInteger(value) && value >= 0;\n}\n\nfunction normalizeHost(host: string): string {\n return host.startsWith('http://') || host.startsWith('https://')\n ? host.replace(/\\/$/, '')\n : `https://${host.replace(/\\/$/, '')}`;\n}\n\n/** Node has Buffer; edge/browser runtimes have btoa. Credentials are ASCII. */\nfunction toBase64(value: string): string {\n if (typeof Buffer !== 'undefined') return Buffer.from(value).toString('base64');\n return btoa(value);\n}\n"]}