@dbx-tools/model 0.9.19 → 0.9.28
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 +61 -10
- package/index.ts +12 -2
- package/lib/exports.d.ts +1 -0
- package/lib/exports.js +2 -0
- package/lib/index.d.ts +12 -2
- package/lib/index.js +9 -2
- package/lib/src/_metadata-contract.d.ts +42 -0
- package/lib/src/_metadata-contract.js +11 -0
- package/lib/src/_metadata-generator.d.ts +7 -0
- package/lib/src/_metadata-generator.js +182 -0
- package/lib/src/_ranking.d.ts +20 -0
- package/lib/src/_ranking.js +226 -0
- package/lib/src/_selection.d.ts +23 -0
- package/lib/src/_selection.js +95 -0
- package/lib/src/classes.d.ts +2 -2
- package/lib/src/classes.js +10 -8
- package/lib/src/classify.d.ts +130 -0
- package/lib/src/classify.js +283 -0
- package/lib/src/fallback.d.ts +4 -4
- package/lib/src/fallback.js +7 -7
- package/lib/src/generated/model-capabilities.json +90 -0
- package/lib/src/generated/model-rate-limits.json +342 -0
- package/lib/src/generated/retired-models.json +32 -0
- package/lib/src/invoke.js +3 -3
- package/lib/src/metadata.d.ts +33 -0
- package/lib/src/metadata.js +120 -0
- package/lib/src/model-catalog.d.ts +4 -4
- package/lib/src/model-catalog.js +19 -24
- package/lib/src/model-client.d.ts +67 -0
- package/lib/src/model-client.js +181 -0
- package/lib/src/policy.d.ts +14 -8
- package/lib/src/policy.js +173 -14
- package/lib/src/python.d.ts +2 -0
- package/lib/src/python.js +3 -0
- package/lib/src/resolve.d.ts +10 -143
- package/lib/src/resolve.js +11 -196
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +15 -5
- package/src/_metadata-contract.ts +56 -0
- package/src/_metadata-generator.ts +199 -0
- package/src/_ranking.ts +260 -0
- package/src/_selection.ts +141 -0
- package/src/classes.ts +11 -7
- package/src/classify.ts +324 -0
- package/src/fallback.ts +11 -7
- package/src/generated/model-capabilities.json +90 -0
- package/src/generated/model-rate-limits.json +342 -0
- package/src/generated/retired-models.json +32 -0
- package/src/invoke.ts +2 -2
- package/src/metadata.ts +189 -0
- package/src/model-catalog.ts +26 -28
- package/src/model-client.ts +311 -0
- package/src/policy.ts +176 -19
- package/src/python.ts +2 -0
- package/src/resolve.ts +24 -270
- package/lib/src/_native.d.ts +0 -14
- package/lib/src/_native.js +0 -124
- package/src/_native.ts +0 -174
package/src/_ranking.ts
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ModelClass as ModelClassValues,
|
|
3
|
+
type ModelClass as ModelClassType,
|
|
4
|
+
type ModelQuery,
|
|
5
|
+
type RankedModel,
|
|
6
|
+
type ServingEndpointSummary,
|
|
7
|
+
} from "@dbx-tools/shared-model/contracts";
|
|
8
|
+
import { toModelDisplayName } from "@dbx-tools/shared-model/display";
|
|
9
|
+
import Fuse from "fuse.js";
|
|
10
|
+
|
|
11
|
+
import { classesAtOrBelow, CHAT_CLASS_ORDER, MODEL_CLASS_ORDER } from "./classes.ts";
|
|
12
|
+
import {
|
|
13
|
+
classifyEndpoints,
|
|
14
|
+
endpointCapabilities,
|
|
15
|
+
supportsToolsByFamily,
|
|
16
|
+
versionTuple,
|
|
17
|
+
} from "./classify.ts";
|
|
18
|
+
import { modelStatusFor } from "./metadata.ts";
|
|
19
|
+
import { modelFamily, modelReasoningEfforts, modelServiceNames } from "./policy.ts";
|
|
20
|
+
|
|
21
|
+
type ModelClass = ModelClassType;
|
|
22
|
+
const ModelClass = ModelClassValues;
|
|
23
|
+
|
|
24
|
+
const DEFAULT_FUZZY_THRESHOLD = 0.4;
|
|
25
|
+
interface NativeRankingOptions {
|
|
26
|
+
readonly includeDeprecated?: boolean;
|
|
27
|
+
readonly modelClass?: ModelClass;
|
|
28
|
+
readonly task?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Rank endpoint records through the TypeScript-owned model policy. */
|
|
32
|
+
export function rankEndpoints(
|
|
33
|
+
endpoints: readonly ServingEndpointSummary[],
|
|
34
|
+
query: ModelQuery = {},
|
|
35
|
+
options: NativeRankingOptions = {},
|
|
36
|
+
): RankedModel[] {
|
|
37
|
+
const originalByName = new Map(endpoints.map((endpoint) => [endpoint.name, endpoint]));
|
|
38
|
+
const normalized = endpoints.map((endpoint) => ({
|
|
39
|
+
...endpoint,
|
|
40
|
+
...(options.modelClass ? { class: options.modelClass } : {}),
|
|
41
|
+
...(options.task ? { task: options.task } : {}),
|
|
42
|
+
}));
|
|
43
|
+
const classified = options.modelClass
|
|
44
|
+
? {
|
|
45
|
+
[ModelClass.ChatThinking]: [],
|
|
46
|
+
[ModelClass.ChatBalanced]: [],
|
|
47
|
+
[ModelClass.ChatFast]: [],
|
|
48
|
+
[ModelClass.Embedding]: [],
|
|
49
|
+
[options.modelClass]: normalized,
|
|
50
|
+
}
|
|
51
|
+
: classifyEndpoints(normalized);
|
|
52
|
+
const requestedClass = options.modelClass ?? query.modelClass;
|
|
53
|
+
const eligible = requestedClass ? classesAtOrBelow(requestedClass) : CHAT_CLASS_ORDER;
|
|
54
|
+
const candidates: RankedModel[] = [];
|
|
55
|
+
for (const modelClass of eligible) {
|
|
56
|
+
for (const endpoint of classified[modelClass]) {
|
|
57
|
+
if (!options.includeDeprecated && endpoint.status?.deprecated) continue;
|
|
58
|
+
if (query.requiresTools && !endpointCapabilities(endpoint).tools) continue;
|
|
59
|
+
candidates.push({ endpoint, modelClass });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const search = query.search?.trim();
|
|
64
|
+
let ranked = candidates;
|
|
65
|
+
if (search) {
|
|
66
|
+
const exact = candidates.find((candidate) => candidate.endpoint.name === search);
|
|
67
|
+
if (exact) {
|
|
68
|
+
ranked = [{ ...exact, score: 0 }];
|
|
69
|
+
} else {
|
|
70
|
+
const threshold = query.threshold ?? DEFAULT_FUZZY_THRESHOLD;
|
|
71
|
+
const searchTokens = tokenize(search);
|
|
72
|
+
const tokenMatches = candidates
|
|
73
|
+
.filter((candidate) => {
|
|
74
|
+
const candidateTokens = new Set(searchableValues(candidate.endpoint).flatMap(tokenize));
|
|
75
|
+
return (
|
|
76
|
+
searchTokens.length > 0 && searchTokens.every((token) => candidateTokens.has(token))
|
|
77
|
+
);
|
|
78
|
+
})
|
|
79
|
+
.map((candidate) => ({ ...candidate, score: 0 }));
|
|
80
|
+
if (tokenMatches.length > 0) {
|
|
81
|
+
ranked = tokenMatches.sort(compareRanked);
|
|
82
|
+
} else {
|
|
83
|
+
const fuse = new Fuse(candidates, {
|
|
84
|
+
keys: [
|
|
85
|
+
"endpoint.name",
|
|
86
|
+
"endpoint.displayName",
|
|
87
|
+
"endpoint.family",
|
|
88
|
+
"endpoint.modelServiceName",
|
|
89
|
+
"endpoint.serviceNames",
|
|
90
|
+
],
|
|
91
|
+
threshold,
|
|
92
|
+
ignoreLocation: true,
|
|
93
|
+
includeScore: true,
|
|
94
|
+
useExtendedSearch: true,
|
|
95
|
+
isCaseSensitive: false,
|
|
96
|
+
});
|
|
97
|
+
const normalizedSearch = tokenize(search).join(" ");
|
|
98
|
+
ranked = normalizedSearch
|
|
99
|
+
? fuse
|
|
100
|
+
.search(normalizedSearch)
|
|
101
|
+
.filter((result) => (result.score ?? 0) <= threshold)
|
|
102
|
+
.map((result) => ({ ...result.item, score: result.score ?? 0 }))
|
|
103
|
+
.sort(compareRanked)
|
|
104
|
+
: [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const restored = ranked.map((result) => ({
|
|
109
|
+
...result,
|
|
110
|
+
endpoint: originalByName.get(result.endpoint.name) ?? result.endpoint,
|
|
111
|
+
}));
|
|
112
|
+
return query.limit === undefined ? restored : restored.slice(0, Math.max(0, query.limit));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Classify chat and embedding endpoints through the TypeScript-owned policy. */
|
|
116
|
+
export function classifyEndpointClasses(
|
|
117
|
+
endpoints: readonly ServingEndpointSummary[],
|
|
118
|
+
): ReadonlyMap<string, ModelClass> {
|
|
119
|
+
const classified = classifyEndpoints(endpoints);
|
|
120
|
+
const result = new Map<string, ModelClass>();
|
|
121
|
+
for (const modelClass of MODEL_CLASS_ORDER) {
|
|
122
|
+
for (const endpoint of classified[modelClass]) result.set(endpoint.name, modelClass);
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Normalize serialized SDK endpoint records without native bindings. */
|
|
128
|
+
export function normalizeEndpoints(endpoints: readonly unknown[]): ServingEndpointSummary[] {
|
|
129
|
+
const summaries = endpoints.flatMap((value) => {
|
|
130
|
+
const endpoint = record(value);
|
|
131
|
+
const name = stringValue(endpoint.name);
|
|
132
|
+
if (!name) return [];
|
|
133
|
+
const entities = arrayValue(record(endpoint.config).served_entities).map(record);
|
|
134
|
+
const identities = [
|
|
135
|
+
name,
|
|
136
|
+
...entities.flatMap((entity) =>
|
|
137
|
+
[
|
|
138
|
+
stringValue(entity.entity_name),
|
|
139
|
+
stringValue(record(entity.foundation_model).name),
|
|
140
|
+
stringValue(record(entity.external_model).name),
|
|
141
|
+
].filter((identity): identity is string => Boolean(identity)),
|
|
142
|
+
),
|
|
143
|
+
];
|
|
144
|
+
const modelServiceName = entities
|
|
145
|
+
.map(
|
|
146
|
+
(entity) =>
|
|
147
|
+
stringValue(record(entity.foundation_model).name) ?? stringValue(entity.entity_name),
|
|
148
|
+
)
|
|
149
|
+
.find(Boolean);
|
|
150
|
+
const family = identities.map(modelFamily).find(Boolean);
|
|
151
|
+
const reasoningEfforts =
|
|
152
|
+
identities.map(modelReasoningEfforts).sort((left, right) => right.length - left.length)[0] ??
|
|
153
|
+
[];
|
|
154
|
+
const profile = entities.map(modelProfile).find(Boolean);
|
|
155
|
+
const summary: ServingEndpointSummary = {
|
|
156
|
+
name,
|
|
157
|
+
displayName: toModelDisplayName(name, providedDisplayName(endpoint, entities)),
|
|
158
|
+
...(family ? { family } : {}),
|
|
159
|
+
...(stringValue(endpoint.task) ? { task: stringValue(endpoint.task) } : {}),
|
|
160
|
+
...(stringValue(record(endpoint.state).ready)
|
|
161
|
+
? { state: stringValue(record(endpoint.state).ready) }
|
|
162
|
+
: {}),
|
|
163
|
+
...(stringValue(endpoint.description)
|
|
164
|
+
? { description: stringValue(endpoint.description) }
|
|
165
|
+
: {}),
|
|
166
|
+
supportsTools: identities.some(supportsToolsByFamily),
|
|
167
|
+
...(profile ? { profile } : {}),
|
|
168
|
+
serviceNames: Object.assign({}, ...identities.map(modelServiceNames)),
|
|
169
|
+
...(modelServiceName ? { modelServiceName } : {}),
|
|
170
|
+
...(reasoningEfforts.length ? { reasoningEfforts } : {}),
|
|
171
|
+
status: modelStatusFor(...identities),
|
|
172
|
+
};
|
|
173
|
+
return [summary];
|
|
174
|
+
});
|
|
175
|
+
const classes = classifyEndpointClasses(summaries);
|
|
176
|
+
return summaries.map((summary) => {
|
|
177
|
+
const modelClass = classes.get(summary.name);
|
|
178
|
+
return modelClass ? { ...summary, class: modelClass } : summary;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function compareRanked(left: RankedModel, right: RankedModel): number {
|
|
183
|
+
const score = Math.round((left.score ?? 0) * 1000) - Math.round((right.score ?? 0) * 1000);
|
|
184
|
+
if (score !== 0) return score;
|
|
185
|
+
const leftVersion = versionTuple(left.endpoint.name);
|
|
186
|
+
const rightVersion = versionTuple(right.endpoint.name);
|
|
187
|
+
for (let index = 0; index < 3; index += 1) {
|
|
188
|
+
const version = rightVersion[index]! - leftVersion[index]!;
|
|
189
|
+
if (version !== 0) return version;
|
|
190
|
+
}
|
|
191
|
+
const variant = modelVariantRank(left.endpoint.name) - modelVariantRank(right.endpoint.name);
|
|
192
|
+
if (variant !== 0) return variant;
|
|
193
|
+
return MODEL_CLASS_ORDER.indexOf(left.modelClass) - MODEL_CLASS_ORDER.indexOf(right.modelClass);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function modelVariantRank(name: string): number {
|
|
197
|
+
if (!/(?:^|[-_.])gpt(?:[-_.]|$)/i.test(name)) return 2;
|
|
198
|
+
if (/(?:^|[-_.])sol(?:[-_.]|$)/i.test(name)) return 0;
|
|
199
|
+
if (/(?:^|[-_.])luna(?:[-_.]|$)/i.test(name)) return 1;
|
|
200
|
+
return 2;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function tokenize(value: string): string[] {
|
|
204
|
+
return value.toLowerCase().match(/[a-z0-9]+/g) ?? [];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function searchableValues(endpoint: ServingEndpointSummary): string[] {
|
|
208
|
+
return [
|
|
209
|
+
endpoint.name,
|
|
210
|
+
endpoint.displayName,
|
|
211
|
+
endpoint.family,
|
|
212
|
+
endpoint.modelServiceName,
|
|
213
|
+
...Object.values(endpoint.serviceNames ?? {}),
|
|
214
|
+
].filter((value): value is string => Boolean(value));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function modelProfile(entity: Record<string, unknown>) {
|
|
218
|
+
const source = record(record(entity.foundation_model).ai_gateway_model_profile);
|
|
219
|
+
const quality = finiteNumber(source.quality);
|
|
220
|
+
const speed = finiteNumber(source.speed);
|
|
221
|
+
const cost = finiteNumber(source.cost);
|
|
222
|
+
if (quality === undefined && speed === undefined && cost === undefined) return undefined;
|
|
223
|
+
return {
|
|
224
|
+
...(quality !== undefined ? { quality } : {}),
|
|
225
|
+
...(speed !== undefined ? { speed } : {}),
|
|
226
|
+
...(cost !== undefined ? { cost } : {}),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function providedDisplayName(
|
|
231
|
+
endpoint: Record<string, unknown>,
|
|
232
|
+
entities: readonly Record<string, unknown>[],
|
|
233
|
+
): string | undefined {
|
|
234
|
+
for (const tag of arrayValue(endpoint.tags).map(record)) {
|
|
235
|
+
if (!["display_name", "displayName", "name"].includes(String(tag.key))) continue;
|
|
236
|
+
const value = stringValue(tag.value);
|
|
237
|
+
if (value) return value;
|
|
238
|
+
}
|
|
239
|
+
return entities.map((entity) => stringValue(record(entity.external_model).name)).find(Boolean);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function record(value: unknown): Record<string, unknown> {
|
|
243
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
244
|
+
? (value as Record<string, unknown>)
|
|
245
|
+
: {};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function arrayValue(value: unknown): unknown[] {
|
|
249
|
+
return Array.isArray(value) ? value : [];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function stringValue(value: unknown): string | undefined {
|
|
253
|
+
if (typeof value !== "string") return undefined;
|
|
254
|
+
const trimmed = value.trim();
|
|
255
|
+
return trimmed || undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function finiteNumber(value: unknown): number | undefined {
|
|
259
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
260
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime-independent model ranking and selection.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import * as object from "@dbx-tools/shared-core/object";
|
|
7
|
+
import type {
|
|
8
|
+
ModelClass,
|
|
9
|
+
ModelQuery,
|
|
10
|
+
RankedModel,
|
|
11
|
+
ServingEndpointSummary,
|
|
12
|
+
} from "@dbx-tools/shared-model/contracts";
|
|
13
|
+
|
|
14
|
+
import { rankEndpoints } from "./_ranking.ts";
|
|
15
|
+
import { FALLBACK_MODEL_IDS, modelsForClass } from "./fallback.ts";
|
|
16
|
+
import type { ResolvedModel, ResolveModelOptions } from "./model-catalog.ts";
|
|
17
|
+
import { endpointSupportsTools } from "./policy.ts";
|
|
18
|
+
|
|
19
|
+
const DEFAULT_MODEL_FAMILY_SEARCH = "gpt";
|
|
20
|
+
|
|
21
|
+
/** Caller intent passed to {@link resolveModel}. */
|
|
22
|
+
export interface ResolveModelInput {
|
|
23
|
+
explicit?: string;
|
|
24
|
+
fuzzy?: boolean;
|
|
25
|
+
threshold?: number;
|
|
26
|
+
requiresTools?: boolean;
|
|
27
|
+
modelClass?: ModelClass;
|
|
28
|
+
fallbacks?: readonly string[];
|
|
29
|
+
liveOnly?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Outcome of {@link resolveModel}: the chosen id plus how it was reached. */
|
|
33
|
+
export interface ResolvedModelSelection {
|
|
34
|
+
modelId: string;
|
|
35
|
+
source: "explicit" | "fuzzy-match" | "class" | "fallback";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Rank a catalogue against a model query, best-first. */
|
|
39
|
+
export function lookupModels(
|
|
40
|
+
endpoints: readonly ServingEndpointSummary[],
|
|
41
|
+
query: ModelQuery = {},
|
|
42
|
+
): RankedModel[] {
|
|
43
|
+
return rankEndpoints(endpoints, query);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Resolve the closest model id in one catalogue snapshot. */
|
|
47
|
+
export function rankModelId(
|
|
48
|
+
endpoints: readonly ServingEndpointSummary[],
|
|
49
|
+
search: string,
|
|
50
|
+
options: ResolveModelOptions = {},
|
|
51
|
+
): ResolvedModel {
|
|
52
|
+
const [top] = lookupModels(endpoints, {
|
|
53
|
+
search,
|
|
54
|
+
limit: 1,
|
|
55
|
+
...(options.threshold !== undefined ? { threshold: options.threshold } : {}),
|
|
56
|
+
...(options.requiresTools !== undefined ? { requiresTools: options.requiresTools } : {}),
|
|
57
|
+
});
|
|
58
|
+
if (!top) return { modelId: search, matched: false };
|
|
59
|
+
return { modelId: top.endpoint.name, matched: true, score: top.score };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Resolve a single model id from a catalogue and caller intent. */
|
|
63
|
+
export function resolveModel(
|
|
64
|
+
endpoints: readonly ServingEndpointSummary[],
|
|
65
|
+
input: ResolveModelInput = {},
|
|
66
|
+
): ResolvedModelSelection {
|
|
67
|
+
if (input.explicit !== undefined) {
|
|
68
|
+
if (input.fuzzy === false) {
|
|
69
|
+
if (input.requiresTools) assertToolSupport(endpoints, input.explicit);
|
|
70
|
+
return { modelId: input.explicit, source: "explicit" };
|
|
71
|
+
}
|
|
72
|
+
const [top] = lookupModels(endpoints, buildQuery(input, input.explicit));
|
|
73
|
+
if (input.requiresTools && !top) {
|
|
74
|
+
throw new Error(`No tool-capable model matches "${input.explicit}"`);
|
|
75
|
+
}
|
|
76
|
+
return { modelId: top?.endpoint.name ?? input.explicit, source: "fuzzy-match" };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (input.modelClass === undefined && input.fallbacks && input.fallbacks.length > 0) {
|
|
80
|
+
const present = new Set(
|
|
81
|
+
endpoints
|
|
82
|
+
.filter((endpoint) => !input.requiresTools || endpointSupportsTools(endpoint))
|
|
83
|
+
.map((endpoint) => endpoint.name),
|
|
84
|
+
);
|
|
85
|
+
const pinned = input.fallbacks.find((id) => present.has(id));
|
|
86
|
+
if (pinned) return { modelId: pinned, source: "fallback" };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const source = input.modelClass !== undefined ? "class" : "fallback";
|
|
90
|
+
if (input.modelClass === undefined) {
|
|
91
|
+
const [preferred] = lookupModels(endpoints, buildQuery(input, DEFAULT_MODEL_FAMILY_SEARCH));
|
|
92
|
+
if (preferred) return { modelId: preferred.endpoint.name, source };
|
|
93
|
+
}
|
|
94
|
+
const [top] = lookupModels(endpoints, buildQuery(input, undefined));
|
|
95
|
+
if (top) return { modelId: top.endpoint.name, source };
|
|
96
|
+
|
|
97
|
+
if (input.liveOnly) {
|
|
98
|
+
throw new Error("No matching live Model Serving endpoint is available");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const floorSource =
|
|
102
|
+
input.modelClass !== undefined ? modelsForClass(input.modelClass) : (input.fallbacks ?? []);
|
|
103
|
+
const floor = object.sequence(floorSource).concat(FALLBACK_MODEL_IDS).distinct().toArray();
|
|
104
|
+
if (input.requiresTools) {
|
|
105
|
+
const available = new Set(
|
|
106
|
+
endpoints.filter(endpointSupportsTools).map((endpoint) => endpoint.name),
|
|
107
|
+
);
|
|
108
|
+
const selected = floor.find((id) => available.has(id));
|
|
109
|
+
if (!selected) throw new Error("No tool-capable model is available");
|
|
110
|
+
return { modelId: selected, source };
|
|
111
|
+
}
|
|
112
|
+
return { modelId: pickFirstAvailable(floor, endpoints), source };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildQuery(input: ResolveModelInput, search: string | undefined): ModelQuery {
|
|
116
|
+
return {
|
|
117
|
+
...(search !== undefined ? { search } : {}),
|
|
118
|
+
...(input.modelClass !== undefined ? { modelClass: input.modelClass } : {}),
|
|
119
|
+
...(input.requiresTools !== undefined ? { requiresTools: input.requiresTools } : {}),
|
|
120
|
+
...(input.threshold !== undefined ? { threshold: input.threshold } : {}),
|
|
121
|
+
limit: 1,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function assertToolSupport(endpoints: readonly ServingEndpointSummary[], modelId: string): void {
|
|
126
|
+
const endpoint = endpoints.find((candidate) => candidate.name === modelId);
|
|
127
|
+
if (!endpoint || !endpointSupportsTools(endpoint)) {
|
|
128
|
+
throw new Error(`Model "${modelId}" does not support function tools`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function pickFirstAvailable(
|
|
133
|
+
candidates: readonly string[],
|
|
134
|
+
endpoints: readonly ServingEndpointSummary[],
|
|
135
|
+
): string {
|
|
136
|
+
const present = new Set(endpoints.map((endpoint) => endpoint.name));
|
|
137
|
+
for (const candidate of candidates) {
|
|
138
|
+
if (present.has(candidate)) return candidate;
|
|
139
|
+
}
|
|
140
|
+
return candidates[0] ?? FALLBACK_MODEL_IDS[0]!;
|
|
141
|
+
}
|
package/src/classes.ts
CHANGED
|
@@ -10,10 +10,14 @@
|
|
|
10
10
|
* @module
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
ModelClass as ModelClassValues,
|
|
15
|
+
type ModelClass as ModelClassType,
|
|
16
|
+
} from "@dbx-tools/shared-model/contracts";
|
|
14
17
|
|
|
15
|
-
type ModelClass =
|
|
16
|
-
const
|
|
18
|
+
type ModelClass = ModelClassType;
|
|
19
|
+
const ModelClass = ModelClassValues;
|
|
20
|
+
const MODEL_CLASSES = new Set<ModelClass>(Object.values(ModelClassValues));
|
|
17
21
|
|
|
18
22
|
/**
|
|
19
23
|
* Chat capability ladder in descending order - most capable
|
|
@@ -50,10 +54,10 @@ export function isChatClass(cls: ModelClass): boolean {
|
|
|
50
54
|
* `"balanced"` / `"fast"` resolves to the corresponding `chat-*` class.
|
|
51
55
|
*/
|
|
52
56
|
export function parseModelClass(value: unknown): ModelClass | null {
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
const prefixed =
|
|
56
|
-
return prefixed
|
|
57
|
+
if (typeof value !== "string") return null;
|
|
58
|
+
if (MODEL_CLASSES.has(value as ModelClass)) return value as ModelClass;
|
|
59
|
+
const prefixed = `chat-${value}` as ModelClass;
|
|
60
|
+
return MODEL_CLASSES.has(prefixed) ? prefixed : null;
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
/**
|