@dbx-tools/model 0.9.31 → 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.
- package/README.md +3 -3
- package/exports.ts +1 -0
- package/index.ts +12 -8
- package/lib/index.d.ts +12 -8
- package/lib/index.js +8 -5
- package/lib/scripts/generate-metadata.d.ts +1 -0
- package/lib/scripts/generate-metadata.js +65 -0
- package/lib/src/_metadata-contract.d.ts +12 -6
- package/lib/src/_metadata-contract.js +3 -1
- package/lib/src/_metadata-generator.d.ts +3 -1
- package/lib/src/_metadata-generator.js +148 -1
- package/lib/src/_ranking.js +39 -12
- package/lib/src/_selection.d.ts +1 -17
- package/lib/src/_selection.js +1 -1
- package/lib/src/classify.d.ts +1 -17
- package/lib/src/classify.js +1 -1
- package/lib/src/generated/model-reasoning.json +147 -0
- package/lib/src/invoke.d.ts +8 -0
- package/lib/src/invoke.js +13 -1
- package/lib/src/metadata-cache.d.ts +84 -0
- package/lib/src/metadata-cache.js +226 -0
- package/lib/src/metadata.d.ts +52 -17
- package/lib/src/metadata.js +251 -18
- package/lib/src/model-catalog.d.ts +1 -19
- package/lib/src/model-catalog.js +9 -2
- package/lib/src/model-client.d.ts +11 -19
- package/lib/src/model-client.js +42 -24
- package/lib/src/reasoning-translation.d.ts +105 -0
- package/lib/src/reasoning-translation.js +444 -0
- package/lib/src/resolve.d.ts +2 -4
- package/lib/src/resolve.js +2 -2
- package/lib/src/serving-wire.d.ts +37 -0
- package/lib/src/serving-wire.js +211 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +110 -17
- package/src/_metadata-contract.ts +17 -7
- package/src/_metadata-generator.ts +160 -0
- package/src/_ranking.ts +40 -11
- package/src/_selection.ts +4 -19
- package/src/classify.ts +2 -18
- package/src/generated/model-reasoning.json +147 -0
- package/src/invoke.ts +16 -0
- package/src/metadata-cache.ts +307 -0
- package/src/metadata.ts +334 -43
- package/src/model-catalog.ts +8 -25
- package/src/model-client.ts +67 -44
- package/src/reasoning-translation.ts +495 -0
- package/src/resolve.ts +10 -11
- package/src/serving-wire.ts +231 -0
- package/lib/src/python.d.ts +0 -2
- package/lib/src/python.js +0 -3
- package/src/python.ts +0 -2
package/src/model-client.ts
CHANGED
|
@@ -8,30 +8,28 @@
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
import {
|
|
11
|
-
|
|
11
|
+
client as authClient,
|
|
12
|
+
profile as authProfile,
|
|
12
13
|
type AuthClient,
|
|
13
14
|
type DatabricksAuthDependencies,
|
|
14
|
-
type DatabricksAuthOptions,
|
|
15
|
-
type DatabricksProfileSummary,
|
|
16
15
|
} from "@dbx-tools/auth";
|
|
16
|
+
import type { DatabricksProfileSummary } from "@dbx-tools/shared-auth";
|
|
17
17
|
import * as log from "@dbx-tools/shared-core/log";
|
|
18
18
|
import * as object from "@dbx-tools/shared-core/object";
|
|
19
19
|
import {
|
|
20
20
|
ModelClass,
|
|
21
|
+
type ModelMetadata,
|
|
21
22
|
type ModelQuery,
|
|
22
23
|
type RankedModel,
|
|
24
|
+
type ResolvedModelSelection,
|
|
25
|
+
type ResolveModelInput,
|
|
23
26
|
type ServingEndpointSummary,
|
|
24
27
|
} from "@dbx-tools/shared-model/contracts";
|
|
25
28
|
|
|
26
29
|
import { normalizeEndpoints } from "./_ranking.ts";
|
|
27
|
-
import {
|
|
28
|
-
lookupModels,
|
|
29
|
-
type ResolveModelInput,
|
|
30
|
-
type ResolvedModelSelection,
|
|
31
|
-
resolveModel,
|
|
32
|
-
} from "./_selection.ts";
|
|
30
|
+
import { lookupModels, resolveModel } from "./_selection.ts";
|
|
33
31
|
import { chatCompletionsUrl, invocationsUrl, responsesUpstreamUrl } from "./invoke.ts";
|
|
34
|
-
import {
|
|
32
|
+
import { modelMetadataFor } from "./metadata.ts";
|
|
35
33
|
import { modelServingApi } from "./policy.ts";
|
|
36
34
|
|
|
37
35
|
const logger = log.logger("model/client");
|
|
@@ -39,17 +37,23 @@ const logger = log.logger("model/client");
|
|
|
39
37
|
/** Default lifetime for one live endpoint catalogue. */
|
|
40
38
|
export const DEFAULT_MODEL_CLIENT_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
41
39
|
|
|
40
|
+
/** Authentication options exposed by the portable model client. */
|
|
41
|
+
export interface ModelAuthOptions {
|
|
42
|
+
/** Databricks profile name. */
|
|
43
|
+
profile?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
42
46
|
/** Construction options for {@link createModelClient}. */
|
|
43
47
|
export interface ModelClientOptions {
|
|
44
48
|
/** Databricks authentication and profile selection options. */
|
|
45
|
-
auth?:
|
|
49
|
+
auth?: ModelAuthOptions;
|
|
46
50
|
/** In-memory endpoint catalogue TTL in milliseconds. */
|
|
47
51
|
cacheTtlMs?: number;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
/** Secret-free runtime identity and cache configuration. */
|
|
51
55
|
export interface ModelClientStatus {
|
|
52
|
-
readonly profile
|
|
56
|
+
readonly profile?: string;
|
|
53
57
|
readonly host: string;
|
|
54
58
|
readonly principal: string;
|
|
55
59
|
readonly workspaceId?: string;
|
|
@@ -86,13 +90,9 @@ export interface ModelRoute {
|
|
|
86
90
|
export interface ModelClient {
|
|
87
91
|
listModels(refresh?: boolean): Promise<ServingEndpointSummary[]>;
|
|
88
92
|
searchModels(query?: ModelQuery, refresh?: boolean): Promise<RankedModel[]>;
|
|
89
|
-
resolveModel(input?: ResolveModelInput, refresh?: boolean): Promise<ResolvedModelSelection>;
|
|
90
93
|
route(input?: ModelRouteInput): Promise<ModelRoute>;
|
|
91
|
-
authenticate(login?: boolean): Promise<Record<string, string>>;
|
|
92
94
|
metadata(model: string | ServingEndpointSummary): ModelMetadata;
|
|
93
95
|
status(): ModelClientStatus;
|
|
94
|
-
profile(name?: string): DatabricksProfileSummary;
|
|
95
|
-
listProfiles(refresh?: boolean): DatabricksProfileSummary[];
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
interface DatabricksModelClient {
|
|
@@ -103,6 +103,42 @@ interface DatabricksModelClient {
|
|
|
103
103
|
request(path: string): Promise<unknown>;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
class AuthenticatedModelClient implements DatabricksModelClient {
|
|
107
|
+
constructor(
|
|
108
|
+
readonly auth: AuthClient,
|
|
109
|
+
private readonly fetcher: typeof globalThis.fetch,
|
|
110
|
+
) {}
|
|
111
|
+
|
|
112
|
+
host(): string {
|
|
113
|
+
return this.auth.host;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
principal(): string {
|
|
117
|
+
return this.auth.principal;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
workspaceId(): string | undefined {
|
|
121
|
+
return this.auth.workspaceId;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async request(path: string): Promise<unknown> {
|
|
125
|
+
const url = new URL(path, `${this.host().replace(/\/$/, "")}/`).toString();
|
|
126
|
+
let headers = await this.auth.headers();
|
|
127
|
+
let response = await this.fetcher(url, { headers });
|
|
128
|
+
if (response.status === 401) {
|
|
129
|
+
headers = await this.auth.headers({ refresh: true });
|
|
130
|
+
response = await this.fetcher(url, { headers });
|
|
131
|
+
}
|
|
132
|
+
const text = await response.text();
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Databricks Model Serving API ${path} returned HTTP ${response.status}: ${text}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return text ? JSON.parse(text) : undefined;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
106
142
|
interface CatalogueEntry {
|
|
107
143
|
readonly promise: Promise<readonly ServingEndpointSummary[]>;
|
|
108
144
|
expiresAt: number;
|
|
@@ -127,18 +163,6 @@ class DefaultModelClient implements ModelClient {
|
|
|
127
163
|
}));
|
|
128
164
|
}
|
|
129
165
|
|
|
130
|
-
async resolveModel(
|
|
131
|
-
input: ResolveModelInput = {},
|
|
132
|
-
refresh = false,
|
|
133
|
-
): Promise<ResolvedModelSelection> {
|
|
134
|
-
const catalogue = await this.catalogue(refresh);
|
|
135
|
-
const resolved = resolveModel(catalogue, input);
|
|
136
|
-
if (isUnmatchedExplicit(catalogue, input, resolved)) {
|
|
137
|
-
return resolveModel(await this.catalogue(true), input);
|
|
138
|
-
}
|
|
139
|
-
return resolved;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
166
|
async route(input: ModelRouteInput = {}): Promise<ModelRoute> {
|
|
143
167
|
const { refresh = false, protocol: requestedProtocol, login, ...selection } = input;
|
|
144
168
|
const effectiveSelection =
|
|
@@ -156,7 +180,7 @@ class DefaultModelClient implements ModelClient {
|
|
|
156
180
|
const protocol = requestedProtocol ?? inferProtocol(endpoint, resolved.modelId);
|
|
157
181
|
const host = this.client.host();
|
|
158
182
|
const url = routeUrl(host, resolved.modelId, protocol);
|
|
159
|
-
const headers = await this.client.auth.
|
|
183
|
+
const headers = await this.client.auth.headers({ login });
|
|
160
184
|
return {
|
|
161
185
|
modelId: resolved.modelId,
|
|
162
186
|
...(endpoint ? { endpoint: cloneEndpoint(endpoint) } : {}),
|
|
@@ -170,17 +194,13 @@ class DefaultModelClient implements ModelClient {
|
|
|
170
194
|
};
|
|
171
195
|
}
|
|
172
196
|
|
|
173
|
-
authenticate(login?: boolean): Promise<Record<string, string>> {
|
|
174
|
-
return this.client.auth.authenticate(login);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
197
|
metadata(model: string | ServingEndpointSummary): ModelMetadata {
|
|
178
198
|
return modelMetadataFor(model);
|
|
179
199
|
}
|
|
180
200
|
|
|
181
201
|
status(): ModelClientStatus {
|
|
182
202
|
return {
|
|
183
|
-
profile: this.client.auth.
|
|
203
|
+
...(this.client.auth.profile ? { profile: this.client.auth.profile } : {}),
|
|
184
204
|
host: this.client.host(),
|
|
185
205
|
principal: this.client.principal(),
|
|
186
206
|
...(this.client.workspaceId() ? { workspaceId: this.client.workspaceId() } : {}),
|
|
@@ -188,14 +208,6 @@ class DefaultModelClient implements ModelClient {
|
|
|
188
208
|
};
|
|
189
209
|
}
|
|
190
210
|
|
|
191
|
-
profile(name?: string): DatabricksProfileSummary {
|
|
192
|
-
return this.client.auth.profile(name);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
listProfiles(refresh = false): DatabricksProfileSummary[] {
|
|
196
|
-
return this.client.auth.listProfiles(refresh);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
211
|
private catalogue(refresh: boolean): Promise<readonly ServingEndpointSummary[]> {
|
|
200
212
|
const key = catalogueKey(this.client);
|
|
201
213
|
if (refresh) catalogueCache.delete(key);
|
|
@@ -234,7 +246,15 @@ class DefaultModelClient implements ModelClient {
|
|
|
234
246
|
/** Create an authentication-aware Databricks model client. */
|
|
235
247
|
export async function createModelClient(options: ModelClientOptions = {}): Promise<ModelClient> {
|
|
236
248
|
const cacheTtlMs = validateCacheTtl(options.cacheTtlMs);
|
|
237
|
-
return new DefaultModelClient(
|
|
249
|
+
return new DefaultModelClient(
|
|
250
|
+
new AuthenticatedModelClient(await authClient.createAuthClient(options.auth), globalThis.fetch),
|
|
251
|
+
cacheTtlMs,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** List configured Databricks profiles outside the model client facade. */
|
|
256
|
+
export function listProfiles(refresh = false): DatabricksProfileSummary[] {
|
|
257
|
+
return authProfile.listProfiles({ refresh });
|
|
238
258
|
}
|
|
239
259
|
|
|
240
260
|
/** @internal Construct a model client around a test or host-owned Databricks client. */
|
|
@@ -251,7 +271,10 @@ export async function createModelClientWithDependencies(
|
|
|
251
271
|
dependencies: DatabricksAuthDependencies,
|
|
252
272
|
): Promise<ModelClient> {
|
|
253
273
|
return new DefaultModelClient(
|
|
254
|
-
|
|
274
|
+
new AuthenticatedModelClient(
|
|
275
|
+
await authClient.createAuthClient(options.auth, dependencies),
|
|
276
|
+
dependencies.fetch ?? globalThis.fetch,
|
|
277
|
+
),
|
|
255
278
|
validateCacheTtl(options.cacheTtlMs),
|
|
256
279
|
);
|
|
257
280
|
}
|
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic reasoning-budget translation for Model Serving and gateway callers.
|
|
3
|
+
*
|
|
4
|
+
* Provider wires disagree on effort labels (`xhigh`, `ultra`, `med`, `none`, …).
|
|
5
|
+
* This module owns one industry-shaped ladder and two parsers:
|
|
6
|
+
*
|
|
7
|
+
* - {@link parseReasoning}: fuzzy-match one label onto {@link ReasoningLevel}
|
|
8
|
+
* - {@link parseReasoningLevels}: unwrap nested / double-encoded error bodies
|
|
9
|
+
* - {@link remapReasoning} / {@link adaptRequestReasoning}: map a request onto a
|
|
10
|
+
* supported ladder and rewrite `reasoning_effort` / `reasoning.effort`
|
|
11
|
+
*
|
|
12
|
+
* Documented model ladders come from the committed HTML snapshot refreshed by
|
|
13
|
+
* `bun run --filter '@dbx-tools/model' metadata` (same cache path as
|
|
14
|
+
* capabilities / rate limits). Error-body parsing is never cached; callers
|
|
15
|
+
* persist learned ladders via the metadata module.
|
|
16
|
+
*
|
|
17
|
+
* Error bodies arrive as complete HTTP JSON whether the request asked for
|
|
18
|
+
* streaming or not (Databricks rejects unsupported efforts with 400 before SSE
|
|
19
|
+
* starts), so {@link parseReasoningLevels} applies to both paths.
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { json } from "@dbx-tools/shared-core";
|
|
25
|
+
import { isRecord } from "@dbx-tools/shared-core/object";
|
|
26
|
+
import * as stringUtils from "@dbx-tools/shared-core/string-utils";
|
|
27
|
+
import Fuse from "fuse.js";
|
|
28
|
+
|
|
29
|
+
import { modelFamily, modelSearchQuery } from "./policy.ts";
|
|
30
|
+
|
|
31
|
+
/** Industry-generic reasoning budget levels, lowest to highest. */
|
|
32
|
+
export type ReasoningLevel = "low" | "medium" | "high" | "extra-high" | "max";
|
|
33
|
+
|
|
34
|
+
/** Runtime values for {@link ReasoningLevel}. */
|
|
35
|
+
export const ReasoningLevel = {
|
|
36
|
+
Low: "low",
|
|
37
|
+
Medium: "medium",
|
|
38
|
+
High: "high",
|
|
39
|
+
ExtraHigh: "extra-high",
|
|
40
|
+
Max: "max",
|
|
41
|
+
} as const satisfies Record<string, ReasoningLevel>;
|
|
42
|
+
|
|
43
|
+
/** Canonical ladder order used when sorting or ranking remaps. */
|
|
44
|
+
export const REASONING_LEVELS: readonly ReasoningLevel[] = [
|
|
45
|
+
ReasoningLevel.Low,
|
|
46
|
+
ReasoningLevel.Medium,
|
|
47
|
+
ReasoningLevel.High,
|
|
48
|
+
ReasoningLevel.ExtraHigh,
|
|
49
|
+
ReasoningLevel.Max,
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/** Model identity → accepted {@link ReasoningLevel} ladder. */
|
|
53
|
+
export type ReasoningModelCatalogue = Readonly<Record<string, readonly ReasoningLevel[]>>;
|
|
54
|
+
|
|
55
|
+
const STANDARD: readonly ReasoningLevel[] = [
|
|
56
|
+
ReasoningLevel.Low,
|
|
57
|
+
ReasoningLevel.Medium,
|
|
58
|
+
ReasoningLevel.High,
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const WITH_EXTRA_HIGH: readonly ReasoningLevel[] = [
|
|
62
|
+
ReasoningLevel.Low,
|
|
63
|
+
ReasoningLevel.Medium,
|
|
64
|
+
ReasoningLevel.High,
|
|
65
|
+
ReasoningLevel.ExtraHigh,
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const WITH_MAX: readonly ReasoningLevel[] = [
|
|
69
|
+
ReasoningLevel.Low,
|
|
70
|
+
ReasoningLevel.Medium,
|
|
71
|
+
ReasoningLevel.High,
|
|
72
|
+
ReasoningLevel.ExtraHigh,
|
|
73
|
+
ReasoningLevel.Max,
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const GPT_5_5_PRO: readonly ReasoningLevel[] = [
|
|
77
|
+
ReasoningLevel.Medium,
|
|
78
|
+
ReasoningLevel.High,
|
|
79
|
+
ReasoningLevel.ExtraHigh,
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Alias table for exact / near-exact labels. Keys are {@link normalizeReasoningToken}
|
|
84
|
+
* results. Provider wires (`xhigh`) and plain English (`ultra`, `med`) both land
|
|
85
|
+
* on the generic ladder.
|
|
86
|
+
*/
|
|
87
|
+
const ALIASES: Readonly<Record<string, ReasoningLevel>> = {
|
|
88
|
+
low: ReasoningLevel.Low,
|
|
89
|
+
l: ReasoningLevel.Low,
|
|
90
|
+
none: ReasoningLevel.Low,
|
|
91
|
+
off: ReasoningLevel.Low,
|
|
92
|
+
disable: ReasoningLevel.Low,
|
|
93
|
+
disabled: ReasoningLevel.Low,
|
|
94
|
+
zero: ReasoningLevel.Low,
|
|
95
|
+
minimal: ReasoningLevel.Low,
|
|
96
|
+
min: ReasoningLevel.Low,
|
|
97
|
+
tiny: ReasoningLevel.Low,
|
|
98
|
+
light: ReasoningLevel.Low,
|
|
99
|
+
medium: ReasoningLevel.Medium,
|
|
100
|
+
med: ReasoningLevel.Medium,
|
|
101
|
+
mid: ReasoningLevel.Medium,
|
|
102
|
+
moderate: ReasoningLevel.Medium,
|
|
103
|
+
default: ReasoningLevel.Medium,
|
|
104
|
+
normal: ReasoningLevel.Medium,
|
|
105
|
+
balanced: ReasoningLevel.Medium,
|
|
106
|
+
high: ReasoningLevel.High,
|
|
107
|
+
hi: ReasoningLevel.High,
|
|
108
|
+
heavy: ReasoningLevel.High,
|
|
109
|
+
"extra-high": ReasoningLevel.ExtraHigh,
|
|
110
|
+
extrahigh: ReasoningLevel.ExtraHigh,
|
|
111
|
+
xhigh: ReasoningLevel.ExtraHigh,
|
|
112
|
+
xh: ReasoningLevel.ExtraHigh,
|
|
113
|
+
ultra: ReasoningLevel.ExtraHigh,
|
|
114
|
+
"ultra-high": ReasoningLevel.ExtraHigh,
|
|
115
|
+
ultrahigh: ReasoningLevel.ExtraHigh,
|
|
116
|
+
max: ReasoningLevel.Max,
|
|
117
|
+
maximum: ReasoningLevel.Max,
|
|
118
|
+
highest: ReasoningLevel.Max,
|
|
119
|
+
full: ReasoningLevel.Max,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const SUPPORTED_VALUES_PATTERN =
|
|
123
|
+
/supported\s+values?\s+are\s*:?\s*(.+?)(?:\.?\s*(?:param|type|code)\b|$)/is;
|
|
124
|
+
const TOKEN_PATTERN = /'([^']+)'|"([^"]+)"|`([^`]+)`|\b([a-z][a-z0-9_-]*)\b/gi;
|
|
125
|
+
const MAX_JSON_UNWRAP_DEPTH = 8;
|
|
126
|
+
|
|
127
|
+
interface AliasDocument {
|
|
128
|
+
readonly token: string;
|
|
129
|
+
readonly level: ReasoningLevel;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const aliasDocuments: readonly AliasDocument[] = Object.entries(ALIASES).map(
|
|
133
|
+
([token, level]) => ({ token, level }),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const aliasFuse = new Fuse(aliasDocuments, {
|
|
137
|
+
keys: ["token"],
|
|
138
|
+
includeScore: true,
|
|
139
|
+
threshold: 0.35,
|
|
140
|
+
ignoreLocation: true,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Fuzzy-match one reasoning label onto {@link ReasoningLevel}.
|
|
145
|
+
*
|
|
146
|
+
* Returns `undefined` when the input is empty or cannot be mapped confidently.
|
|
147
|
+
*/
|
|
148
|
+
export function parseReasoning(value: unknown): ReasoningLevel | undefined {
|
|
149
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
const normalized = normalizeReasoningToken(String(value));
|
|
153
|
+
if (!normalized) return undefined;
|
|
154
|
+
const exact = ALIASES[normalized] ?? ALIASES[normalized.replace(/-/g, "")];
|
|
155
|
+
if (exact) return exact;
|
|
156
|
+
const [match] = aliasFuse.search(normalized);
|
|
157
|
+
if (!match || (match.score ?? 1) > 0.35) return undefined;
|
|
158
|
+
return match.item.level;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Extract supported reasoning levels from an upstream error body.
|
|
163
|
+
*
|
|
164
|
+
* Accepts raw response text, already-parsed JSON, or Databricks envelopes that
|
|
165
|
+
* nest JSON strings several times. Returns `[]` when no supported-values list
|
|
166
|
+
* can be recovered. Results are not cached.
|
|
167
|
+
*/
|
|
168
|
+
export function parseReasoningLevels(body: unknown): ReasoningLevel[] {
|
|
169
|
+
const levels: ReasoningLevel[] = [];
|
|
170
|
+
const seen = new Set<ReasoningLevel>();
|
|
171
|
+
for (const text of collectTextLeaves(body)) {
|
|
172
|
+
for (const token of extractSupportedTokens(text)) {
|
|
173
|
+
const level = parseReasoning(token);
|
|
174
|
+
if (!level || seen.has(level)) continue;
|
|
175
|
+
seen.add(level);
|
|
176
|
+
levels.push(level);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return levels;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Look up documented levels for a model identity inside a catalogue.
|
|
184
|
+
*
|
|
185
|
+
* Matches exact catalogue keys and suffix identities.
|
|
186
|
+
*/
|
|
187
|
+
export function documentedReasoningLevels(
|
|
188
|
+
model: string,
|
|
189
|
+
catalogue: ReasoningModelCatalogue,
|
|
190
|
+
): ReasoningLevel[] | undefined {
|
|
191
|
+
const keys = catalogueKeysFor(model);
|
|
192
|
+
for (const key of keys) {
|
|
193
|
+
const levels = catalogue[key];
|
|
194
|
+
if (levels?.length) return [...levels];
|
|
195
|
+
}
|
|
196
|
+
for (const [key, levels] of Object.entries(catalogue)) {
|
|
197
|
+
if (keys.some((candidate) => key.endsWith(candidate) || candidate.endsWith(key))) {
|
|
198
|
+
return [...levels];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Hard-coded family defaults used when documentation has no entry.
|
|
206
|
+
*
|
|
207
|
+
* Unknown families return the standard low/medium/high ladder.
|
|
208
|
+
*/
|
|
209
|
+
export function defaultReasoningLevels(model: string): ReasoningLevel[] {
|
|
210
|
+
const family = modelFamily(model)?.toLowerCase();
|
|
211
|
+
const normalized = model.toLowerCase();
|
|
212
|
+
if (family === "grok" || normalized.includes("grok")) return [...WITH_EXTRA_HIGH];
|
|
213
|
+
if (family === "gemini" || normalized.includes("gemini")) return [...STANDARD];
|
|
214
|
+
if (family === "claude" || normalized.includes("claude")) return [...WITH_MAX];
|
|
215
|
+
if (family === "glm" || normalized.includes("glm")) {
|
|
216
|
+
return [ReasoningLevel.High, ReasoningLevel.Max];
|
|
217
|
+
}
|
|
218
|
+
if (
|
|
219
|
+
family === "deepseek" ||
|
|
220
|
+
normalized.includes("deepseek") ||
|
|
221
|
+
family === "kimi" ||
|
|
222
|
+
normalized.includes("kimi")
|
|
223
|
+
) {
|
|
224
|
+
return [ReasoningLevel.Low, ReasoningLevel.High, ReasoningLevel.Max];
|
|
225
|
+
}
|
|
226
|
+
if (family === "inkling" || normalized.includes("inkling")) return [...WITH_MAX];
|
|
227
|
+
if (family === "gpt" || normalized.includes("gpt") || normalized.includes("codex")) {
|
|
228
|
+
if (normalized.includes("oss")) return [...STANDARD];
|
|
229
|
+
if (/(?:^|[-_.])5[-_.]?5(?:[-_.]|$)/.test(normalized) && normalized.includes("pro")) {
|
|
230
|
+
return [...GPT_5_5_PRO];
|
|
231
|
+
}
|
|
232
|
+
if (
|
|
233
|
+
/(?:^|[-_.])5[-_.]?6(?:[-_.]|$)/.test(normalized) ||
|
|
234
|
+
/(?:^|[-_.])6(?:[-_.]|$)/.test(normalized)
|
|
235
|
+
) {
|
|
236
|
+
return [...WITH_MAX];
|
|
237
|
+
}
|
|
238
|
+
return [...STANDARD];
|
|
239
|
+
}
|
|
240
|
+
if (/(?:^|[-_./])o(?:1|3|4)(?:[-_./]|$)/.test(model) || normalized.includes("codex")) {
|
|
241
|
+
return [...STANDARD];
|
|
242
|
+
}
|
|
243
|
+
return [...STANDARD];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Prefer a documentation catalogue when provided, otherwise
|
|
248
|
+
* {@link defaultReasoningLevels}.
|
|
249
|
+
*/
|
|
250
|
+
export function reasoningLevelsFor(
|
|
251
|
+
model: string,
|
|
252
|
+
catalogue?: ReasoningModelCatalogue,
|
|
253
|
+
): ReasoningLevel[] {
|
|
254
|
+
if (catalogue) {
|
|
255
|
+
const documented = documentedReasoningLevels(model, catalogue);
|
|
256
|
+
if (documented?.length) return documented;
|
|
257
|
+
}
|
|
258
|
+
return defaultReasoningLevels(model);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Map a generic level onto the most common provider wire token.
|
|
263
|
+
*
|
|
264
|
+
* `extra-high` becomes `xhigh` because that is what Databricks / OpenAI error
|
|
265
|
+
* messages advertise today.
|
|
266
|
+
*/
|
|
267
|
+
export function formatReasoning(level: ReasoningLevel): string {
|
|
268
|
+
return level === ReasoningLevel.ExtraHigh ? "xhigh" : level;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Wire tokens that mean "do not reason" rather than "use a low budget". */
|
|
272
|
+
const DISABLED_REASONING_TOKENS = new Set(["none", "off", "disable", "disabled"]);
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Remap a requested effort onto the nearest supported {@link ReasoningLevel}.
|
|
276
|
+
*
|
|
277
|
+
* `none` / `off` (and disable aliases) are not a low budget: when the model
|
|
278
|
+
* requires reasoning, they snap to the supported level closest to
|
|
279
|
+
* {@link ReasoningLevel.Medium}. Other requests prefer the highest allowed
|
|
280
|
+
* level at or below the request, otherwise the lowest allowed level.
|
|
281
|
+
*
|
|
282
|
+
* Returns `undefined` when the request cannot be parsed or `levels` is empty.
|
|
283
|
+
*/
|
|
284
|
+
export function remapReasoning(
|
|
285
|
+
requested: unknown,
|
|
286
|
+
levels: readonly ReasoningLevel[],
|
|
287
|
+
): ReasoningLevel | undefined {
|
|
288
|
+
if (levels.length === 0) return undefined;
|
|
289
|
+
if (isDisabledReasoningToken(requested)) {
|
|
290
|
+
return nearestReasoning(ReasoningLevel.Medium, levels);
|
|
291
|
+
}
|
|
292
|
+
const parsed = parseReasoning(requested);
|
|
293
|
+
if (!parsed) return undefined;
|
|
294
|
+
if (levels.includes(parsed)) return parsed;
|
|
295
|
+
const requestedIndex = REASONING_LEVELS.indexOf(parsed);
|
|
296
|
+
let best: ReasoningLevel | undefined;
|
|
297
|
+
let bestIndex = Number.NEGATIVE_INFINITY;
|
|
298
|
+
for (const level of levels) {
|
|
299
|
+
const index = REASONING_LEVELS.indexOf(level);
|
|
300
|
+
if (index <= requestedIndex && index > bestIndex) {
|
|
301
|
+
best = level;
|
|
302
|
+
bestIndex = index;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return best ?? levels[0];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function isDisabledReasoningToken(value: unknown): boolean {
|
|
309
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
return DISABLED_REASONING_TOKENS.has(normalizeReasoningToken(String(value)));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Closest supported level to `target`; ties prefer the higher budget. */
|
|
316
|
+
function nearestReasoning(
|
|
317
|
+
target: ReasoningLevel,
|
|
318
|
+
levels: readonly ReasoningLevel[],
|
|
319
|
+
): ReasoningLevel | undefined {
|
|
320
|
+
if (levels.length === 0) return undefined;
|
|
321
|
+
if (levels.includes(target)) return target;
|
|
322
|
+
const targetIndex = REASONING_LEVELS.indexOf(target);
|
|
323
|
+
let best: ReasoningLevel | undefined;
|
|
324
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
325
|
+
for (const level of levels) {
|
|
326
|
+
const distance = Math.abs(REASONING_LEVELS.indexOf(level) - targetIndex);
|
|
327
|
+
if (
|
|
328
|
+
distance < bestDistance ||
|
|
329
|
+
(distance === bestDistance &&
|
|
330
|
+
REASONING_LEVELS.indexOf(level) > REASONING_LEVELS.indexOf(best ?? level))
|
|
331
|
+
) {
|
|
332
|
+
best = level;
|
|
333
|
+
bestDistance = distance;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return best;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Result of adapting reasoning fields on a gateway request body. */
|
|
340
|
+
export interface AdaptedRequestReasoning {
|
|
341
|
+
/** Body with remapped wire tokens (same reference when unchanged). */
|
|
342
|
+
readonly body: Readonly<Record<string, unknown>>;
|
|
343
|
+
/** True when any reasoning field was rewritten. */
|
|
344
|
+
readonly changed: boolean;
|
|
345
|
+
/** Wire token written when a reasoning field was present and remapped. */
|
|
346
|
+
readonly wireEffort?: string;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Rewrite `reasoning_effort` / `reasoning.effort` onto a supported ladder.
|
|
351
|
+
*
|
|
352
|
+
* Model-agnostic: callers supply the allowed levels (learned, documented, or
|
|
353
|
+
* defaults). Bodies without reasoning fields are returned unchanged.
|
|
354
|
+
*/
|
|
355
|
+
export function adaptRequestReasoning(
|
|
356
|
+
body: Readonly<Record<string, unknown>>,
|
|
357
|
+
levels: readonly ReasoningLevel[],
|
|
358
|
+
): AdaptedRequestReasoning {
|
|
359
|
+
if (levels.length === 0) return { body, changed: false };
|
|
360
|
+
|
|
361
|
+
const topLevel = body.reasoning_effort;
|
|
362
|
+
const reasoning = isRecord(body.reasoning) ? body.reasoning : undefined;
|
|
363
|
+
const nested = reasoning?.effort;
|
|
364
|
+
const requested = topLevel ?? nested;
|
|
365
|
+
if (requested === undefined) return { body, changed: false };
|
|
366
|
+
|
|
367
|
+
const remapped = remapReasoning(requested, levels);
|
|
368
|
+
if (!remapped) return { body, changed: false };
|
|
369
|
+
const wire = formatReasoning(remapped);
|
|
370
|
+
|
|
371
|
+
let changed = false;
|
|
372
|
+
let next: Record<string, unknown> = { ...body };
|
|
373
|
+
|
|
374
|
+
if (typeof topLevel === "string" || typeof topLevel === "number") {
|
|
375
|
+
if (String(topLevel) !== wire) {
|
|
376
|
+
next = { ...next, reasoning_effort: wire };
|
|
377
|
+
changed = true;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (reasoning && (typeof nested === "string" || typeof nested === "number")) {
|
|
381
|
+
if (String(nested) !== wire) {
|
|
382
|
+
next = { ...next, reasoning: { ...reasoning, effort: wire } };
|
|
383
|
+
changed = true;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return changed ? { body: next, changed: true, wireEffort: wire } : { body, changed: false, wireEffort: wire };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Normalize labels for alias lookup (`Extra High` → `extra-high`). */
|
|
391
|
+
export function normalizeReasoningToken(value: string): string {
|
|
392
|
+
return (
|
|
393
|
+
stringUtils
|
|
394
|
+
.trimToNull(value)
|
|
395
|
+
?.toLowerCase()
|
|
396
|
+
.replace(/[_\s]+/g, "-")
|
|
397
|
+
.replace(/[^a-z0-9-]/g, "")
|
|
398
|
+
.replace(/-+/g, "-")
|
|
399
|
+
.replace(/^-|-$/g, "") ?? ""
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Deduplicate levels while preserving first-seen order. */
|
|
404
|
+
export function uniqueReasoningLevels(levels: readonly ReasoningLevel[]): ReasoningLevel[] {
|
|
405
|
+
const seen = new Set<ReasoningLevel>();
|
|
406
|
+
const ordered: ReasoningLevel[] = [];
|
|
407
|
+
for (const level of levels) {
|
|
408
|
+
if (seen.has(level)) continue;
|
|
409
|
+
seen.add(level);
|
|
410
|
+
ordered.push(level);
|
|
411
|
+
}
|
|
412
|
+
return ordered;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function extractSupportedTokens(text: string): string[] {
|
|
416
|
+
const match = SUPPORTED_VALUES_PATTERN.exec(text);
|
|
417
|
+
if (!match?.[1]) return [];
|
|
418
|
+
return tokensFromFragment(match[1]);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function catalogueKeysFor(model: string): string[] {
|
|
422
|
+
const keys = new Set<string>();
|
|
423
|
+
const direct = modelCatalogueKey(model);
|
|
424
|
+
if (direct) keys.add(direct);
|
|
425
|
+
const stripped = model
|
|
426
|
+
.toLowerCase()
|
|
427
|
+
.replace(/^databricks\//, "")
|
|
428
|
+
.replace(/^system\.ai\./, "")
|
|
429
|
+
.replace(/^databricks-/, "");
|
|
430
|
+
const strippedKey = modelCatalogueKey(stripped);
|
|
431
|
+
if (strippedKey) keys.add(strippedKey);
|
|
432
|
+
const withPrefix = modelCatalogueKey(`databricks-${stripped}`);
|
|
433
|
+
if (withPrefix) keys.add(withPrefix);
|
|
434
|
+
return [...keys];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function modelCatalogueKey(name: string): string | undefined {
|
|
438
|
+
const trimmed = stringUtils.trimToNull(name);
|
|
439
|
+
if (!trimmed) return undefined;
|
|
440
|
+
return (
|
|
441
|
+
modelSearchQuery(trimmed)?.replaceAll(" ", "-") ??
|
|
442
|
+
trimmed
|
|
443
|
+
.toLowerCase()
|
|
444
|
+
.replace(/^databricks-/, "")
|
|
445
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function tokensFromFragment(fragment: string): string[] {
|
|
450
|
+
const tokens: string[] = [];
|
|
451
|
+
for (const part of fragment.matchAll(TOKEN_PATTERN)) {
|
|
452
|
+
const token = part[1] ?? part[2] ?? part[3] ?? part[4];
|
|
453
|
+
if (!token || token === "and" || token === "or" || token === "of") continue;
|
|
454
|
+
tokens.push(token.replace(/^["'`]+|["'`]+$/g, ""));
|
|
455
|
+
}
|
|
456
|
+
return tokens;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Walk a value tree, unwrapping nested JSON strings, and collect text leaves
|
|
461
|
+
* that may contain a supported-values sentence.
|
|
462
|
+
*/
|
|
463
|
+
function collectTextLeaves(value: unknown, depth = 0): string[] {
|
|
464
|
+
if (value == null || depth > MAX_JSON_UNWRAP_DEPTH) return [];
|
|
465
|
+
if (typeof value === "string") {
|
|
466
|
+
const trimmed = value.trim();
|
|
467
|
+
if (!trimmed) return [];
|
|
468
|
+
const parsed = tryUnwrapJson(trimmed);
|
|
469
|
+
if (parsed !== undefined && parsed !== trimmed) {
|
|
470
|
+
return collectTextLeaves(parsed, depth + 1);
|
|
471
|
+
}
|
|
472
|
+
return [trimmed];
|
|
473
|
+
}
|
|
474
|
+
if (typeof value === "number" || typeof value === "boolean") return [String(value)];
|
|
475
|
+
if (Array.isArray(value)) {
|
|
476
|
+
return value.flatMap((entry) => collectTextLeaves(entry, depth + 1));
|
|
477
|
+
}
|
|
478
|
+
if (!isRecord(value)) return [];
|
|
479
|
+
return Object.values(value).flatMap((entry) => collectTextLeaves(entry, depth + 1));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function tryUnwrapJson(text: string): unknown {
|
|
483
|
+
const asRecord = json.parseRecord(text);
|
|
484
|
+
if (asRecord) return asRecord;
|
|
485
|
+
const parsed = json.parse(text);
|
|
486
|
+
if (parsed !== undefined) return parsed;
|
|
487
|
+
if (text.includes('\\"') || text.includes("\\n")) {
|
|
488
|
+
const unescaped = text.replace(/\\"/g, '"').replace(/\\n/g, "\n").replace(/\\\\/g, "\\");
|
|
489
|
+
if (unescaped !== text) {
|
|
490
|
+
const nested = json.parse(unescaped);
|
|
491
|
+
if (nested !== undefined) return nested;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|