@dbx-tools/model 0.9.21 → 0.9.29

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 (86) hide show
  1. package/README.md +61 -10
  2. package/index.ts +12 -2
  3. package/lib/exports.d.ts +1 -0
  4. package/lib/exports.js +2 -0
  5. package/lib/index.d.ts +12 -2
  6. package/lib/index.js +9 -2
  7. package/lib/src/_metadata-contract.d.ts +42 -0
  8. package/lib/src/_metadata-contract.js +11 -0
  9. package/lib/src/_metadata-generator.d.ts +7 -0
  10. package/lib/src/_metadata-generator.js +182 -0
  11. package/lib/src/_ranking.d.ts +20 -0
  12. package/lib/src/_ranking.js +226 -0
  13. package/lib/src/_selection.d.ts +23 -0
  14. package/lib/src/_selection.js +95 -0
  15. package/lib/src/classes.d.ts +2 -2
  16. package/lib/src/classes.js +10 -8
  17. package/lib/src/classify.d.ts +130 -0
  18. package/lib/src/classify.js +283 -0
  19. package/lib/src/fallback.d.ts +4 -4
  20. package/lib/src/fallback.js +7 -7
  21. package/lib/src/generated/model-capabilities.json +90 -0
  22. package/lib/src/generated/model-rate-limits.json +342 -0
  23. package/lib/src/generated/retired-models.json +32 -0
  24. package/lib/src/invoke.js +3 -3
  25. package/lib/src/metadata.d.ts +33 -0
  26. package/lib/src/metadata.js +120 -0
  27. package/lib/src/model-catalog.d.ts +4 -4
  28. package/lib/src/model-catalog.js +19 -24
  29. package/lib/src/model-client.d.ts +67 -0
  30. package/lib/src/model-client.js +181 -0
  31. package/lib/src/policy.d.ts +14 -8
  32. package/lib/src/policy.js +173 -14
  33. package/lib/src/python.d.ts +2 -0
  34. package/lib/src/python.js +3 -0
  35. package/lib/src/resolve.d.ts +10 -143
  36. package/lib/src/resolve.js +11 -196
  37. package/lib/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +15 -5
  39. package/src/_metadata-contract.ts +56 -0
  40. package/src/_metadata-generator.ts +199 -0
  41. package/src/_ranking.ts +260 -0
  42. package/src/_selection.ts +141 -0
  43. package/src/classes.ts +11 -7
  44. package/src/classify.ts +324 -0
  45. package/src/fallback.ts +11 -7
  46. package/src/generated/model-capabilities.json +90 -0
  47. package/src/generated/model-rate-limits.json +342 -0
  48. package/src/generated/retired-models.json +32 -0
  49. package/src/invoke.ts +2 -2
  50. package/src/metadata.ts +189 -0
  51. package/src/model-catalog.ts +26 -28
  52. package/src/model-client.ts +311 -0
  53. package/src/policy.ts +176 -19
  54. package/src/python.ts +2 -0
  55. package/src/resolve.ts +24 -270
  56. package/._LICENSE +0 -0
  57. package/._README.md +0 -0
  58. package/._index.ts +0 -0
  59. package/._package.json +0 -0
  60. package/lib/._index.d.ts +0 -0
  61. package/lib/._index.js +0 -0
  62. package/lib/._tsconfig.tsbuildinfo +0 -0
  63. package/lib/src/.__native.d.ts +0 -0
  64. package/lib/src/.__native.js +0 -0
  65. package/lib/src/._classes.d.ts +0 -0
  66. package/lib/src/._classes.js +0 -0
  67. package/lib/src/._fallback.d.ts +0 -0
  68. package/lib/src/._fallback.js +0 -0
  69. package/lib/src/._invoke.d.ts +0 -0
  70. package/lib/src/._invoke.js +0 -0
  71. package/lib/src/._model-catalog.d.ts +0 -0
  72. package/lib/src/._model-catalog.js +0 -0
  73. package/lib/src/._policy.d.ts +0 -0
  74. package/lib/src/._policy.js +0 -0
  75. package/lib/src/._resolve.d.ts +0 -0
  76. package/lib/src/._resolve.js +0 -0
  77. package/lib/src/_native.d.ts +0 -14
  78. package/lib/src/_native.js +0 -124
  79. package/src/.__native.ts +0 -0
  80. package/src/._classes.ts +0 -0
  81. package/src/._fallback.ts +0 -0
  82. package/src/._invoke.ts +0 -0
  83. package/src/._model-catalog.ts +0 -0
  84. package/src/._policy.ts +0 -0
  85. package/src/._resolve.ts +0 -0
  86. package/src/_native.ts +0 -174
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Authentication-aware Databricks Model Serving client.
3
+ *
4
+ * This is the portable entry point used by generated Python bindings. It keeps
5
+ * authentication, endpoint discovery, caching, normalization, ranking, route
6
+ * selection, and published metadata in Node while exposing one small client.
7
+ *
8
+ * @module
9
+ */
10
+ import {
11
+ DatabricksClient,
12
+ type AuthClient,
13
+ type DatabricksAuthDependencies,
14
+ type DatabricksAuthOptions,
15
+ type DatabricksProfileSummary,
16
+ } from "@dbx-tools/auth";
17
+ import * as log from "@dbx-tools/shared-core/log";
18
+ import * as object from "@dbx-tools/shared-core/object";
19
+ import {
20
+ ModelClass,
21
+ type ModelQuery,
22
+ type RankedModel,
23
+ type ServingEndpointSummary,
24
+ } from "@dbx-tools/shared-model/contracts";
25
+
26
+ import { normalizeEndpoints } from "./_ranking.ts";
27
+ import {
28
+ lookupModels,
29
+ type ResolveModelInput,
30
+ type ResolvedModelSelection,
31
+ resolveModel,
32
+ } from "./_selection.ts";
33
+ import { chatCompletionsUrl, invocationsUrl, responsesUpstreamUrl } from "./invoke.ts";
34
+ import { type ModelMetadata, modelMetadataFor } from "./metadata.ts";
35
+ import { modelServingApi } from "./policy.ts";
36
+
37
+ const logger = log.logger("model/client");
38
+
39
+ /** Default lifetime for one live endpoint catalogue. */
40
+ export const DEFAULT_MODEL_CLIENT_CACHE_TTL_MS = 5 * 60 * 1000;
41
+
42
+ /** Construction options for {@link createModelClient}. */
43
+ export interface ModelClientOptions {
44
+ /** Databricks authentication and profile selection options. */
45
+ auth?: DatabricksAuthOptions;
46
+ /** In-memory endpoint catalogue TTL in milliseconds. */
47
+ cacheTtlMs?: number;
48
+ }
49
+
50
+ /** Secret-free runtime identity and cache configuration. */
51
+ export interface ModelClientStatus {
52
+ readonly profile: string;
53
+ readonly host: string;
54
+ readonly principal: string;
55
+ readonly workspaceId?: string;
56
+ readonly cacheTtlMs: number;
57
+ }
58
+
59
+ /** Serving protocol selected for one resolved model. */
60
+ export type ModelProtocol = "chat" | "responses" | "embeddings";
61
+
62
+ /** Model selection plus route controls. */
63
+ export interface ModelRouteInput extends ResolveModelInput {
64
+ /** Force a live endpoint catalogue refresh before selection. */
65
+ refresh?: boolean;
66
+ /** Requested upstream protocol. Defaults from endpoint task and model policy. */
67
+ protocol?: ModelProtocol;
68
+ /** Whether authentication may run an interactive login. */
69
+ login?: boolean;
70
+ }
71
+
72
+ /** Fully resolved Databricks route for a model request. */
73
+ export interface ModelRoute {
74
+ readonly modelId: string;
75
+ readonly endpoint?: ServingEndpointSummary;
76
+ readonly source: ResolvedModelSelection["source"];
77
+ readonly protocol: ModelProtocol;
78
+ readonly host: string;
79
+ readonly apiBase: string;
80
+ readonly url: string;
81
+ readonly headers: Readonly<Record<string, string>>;
82
+ readonly metadata: ModelMetadata;
83
+ }
84
+
85
+ /** Stable model facade shared by Node and generated language bindings. */
86
+ export interface ModelClient {
87
+ listModels(refresh?: boolean): Promise<ServingEndpointSummary[]>;
88
+ searchModels(query?: ModelQuery, refresh?: boolean): Promise<RankedModel[]>;
89
+ resolveModel(input?: ResolveModelInput, refresh?: boolean): Promise<ResolvedModelSelection>;
90
+ route(input?: ModelRouteInput): Promise<ModelRoute>;
91
+ authenticate(login?: boolean): Promise<Record<string, string>>;
92
+ metadata(model: string | ServingEndpointSummary): ModelMetadata;
93
+ status(): ModelClientStatus;
94
+ profile(name?: string): DatabricksProfileSummary;
95
+ listProfiles(refresh?: boolean): DatabricksProfileSummary[];
96
+ }
97
+
98
+ interface DatabricksModelClient {
99
+ readonly auth: AuthClient;
100
+ host(): string;
101
+ principal(): string;
102
+ workspaceId(): string | undefined;
103
+ request(path: string): Promise<unknown>;
104
+ }
105
+
106
+ interface CatalogueEntry {
107
+ readonly promise: Promise<readonly ServingEndpointSummary[]>;
108
+ expiresAt: number;
109
+ }
110
+
111
+ const catalogueCache = new Map<string, CatalogueEntry>();
112
+
113
+ class DefaultModelClient implements ModelClient {
114
+ constructor(
115
+ private readonly client: DatabricksModelClient,
116
+ private readonly cacheTtlMs: number,
117
+ ) {}
118
+
119
+ async listModels(refresh = false): Promise<ServingEndpointSummary[]> {
120
+ return cloneEndpoints(await this.catalogue(refresh));
121
+ }
122
+
123
+ async searchModels(query: ModelQuery = {}, refresh = false): Promise<RankedModel[]> {
124
+ return lookupModels(await this.catalogue(refresh), query).map(({ endpoint, ...ranked }) => ({
125
+ ...ranked,
126
+ endpoint: cloneEndpoint(endpoint),
127
+ }));
128
+ }
129
+
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
+ async route(input: ModelRouteInput = {}): Promise<ModelRoute> {
143
+ const { refresh = false, protocol: requestedProtocol, login, ...selection } = input;
144
+ const effectiveSelection =
145
+ requestedProtocol === "embeddings" && selection.modelClass === undefined
146
+ ? { ...selection, modelClass: ModelClass.Embedding }
147
+ : selection;
148
+ const catalogue = await this.catalogue(refresh);
149
+ let resolved = resolveModel(catalogue, effectiveSelection);
150
+ if (isUnmatchedExplicit(catalogue, effectiveSelection, resolved)) {
151
+ resolved = resolveModel(await this.catalogue(true), effectiveSelection);
152
+ }
153
+ const endpoint = (await this.catalogue(false)).find(
154
+ (candidate) => candidate.name === resolved.modelId,
155
+ );
156
+ const protocol = requestedProtocol ?? inferProtocol(endpoint, resolved.modelId);
157
+ const host = this.client.host();
158
+ const url = routeUrl(host, resolved.modelId, protocol);
159
+ const headers = await this.client.auth.requestHeadersForUrl(url, login);
160
+ return {
161
+ modelId: resolved.modelId,
162
+ ...(endpoint ? { endpoint: cloneEndpoint(endpoint) } : {}),
163
+ source: resolved.source,
164
+ protocol,
165
+ host,
166
+ apiBase: new URL("serving-endpoints", `${host.replace(/\/$/, "")}/`).toString(),
167
+ url,
168
+ headers,
169
+ metadata: modelMetadataFor(endpoint ?? resolved.modelId),
170
+ };
171
+ }
172
+
173
+ authenticate(login?: boolean): Promise<Record<string, string>> {
174
+ return this.client.auth.authenticate(login);
175
+ }
176
+
177
+ metadata(model: string | ServingEndpointSummary): ModelMetadata {
178
+ return modelMetadataFor(model);
179
+ }
180
+
181
+ status(): ModelClientStatus {
182
+ return {
183
+ profile: this.client.auth.status().profile,
184
+ host: this.client.host(),
185
+ principal: this.client.principal(),
186
+ ...(this.client.workspaceId() ? { workspaceId: this.client.workspaceId() } : {}),
187
+ cacheTtlMs: this.cacheTtlMs,
188
+ };
189
+ }
190
+
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
+ private catalogue(refresh: boolean): Promise<readonly ServingEndpointSummary[]> {
200
+ const key = catalogueKey(this.client);
201
+ if (refresh) catalogueCache.delete(key);
202
+ const current = catalogueCache.get(key);
203
+ if (current && Date.now() < current.expiresAt) return current.promise;
204
+ const promise = this.fetchCatalogue();
205
+ const entry: CatalogueEntry = { promise, expiresAt: Number.POSITIVE_INFINITY };
206
+ catalogueCache.set(key, entry);
207
+ void promise.then(
208
+ () => {
209
+ entry.expiresAt = Date.now() + this.cacheTtlMs;
210
+ },
211
+ () => {
212
+ if (catalogueCache.get(key) === entry) catalogueCache.delete(key);
213
+ },
214
+ );
215
+ return promise;
216
+ }
217
+
218
+ private async fetchCatalogue(): Promise<readonly ServingEndpointSummary[]> {
219
+ const startedAt = Date.now();
220
+ const response = await this.client.request("/api/2.0/serving-endpoints");
221
+ if (!object.isRecord(response) || !Array.isArray(response.endpoints)) {
222
+ throw new Error("Databricks serving-endpoints response is missing an endpoints array");
223
+ }
224
+ const endpoints = normalizeEndpoints(response.endpoints);
225
+ logger.debug("listed", {
226
+ count: endpoints.length,
227
+ host: this.client.host(),
228
+ elapsedMs: Date.now() - startedAt,
229
+ });
230
+ return endpoints;
231
+ }
232
+ }
233
+
234
+ /** Create an authentication-aware Databricks model client. */
235
+ export async function createModelClient(options: ModelClientOptions = {}): Promise<ModelClient> {
236
+ const cacheTtlMs = validateCacheTtl(options.cacheTtlMs);
237
+ return new DefaultModelClient(await DatabricksClient.create(options.auth), cacheTtlMs);
238
+ }
239
+
240
+ /** @internal Construct a model client around a test or host-owned Databricks client. */
241
+ export function createModelClientWithDatabricksClient(
242
+ client: DatabricksModelClient,
243
+ cacheTtlMs = DEFAULT_MODEL_CLIENT_CACHE_TTL_MS,
244
+ ): ModelClient {
245
+ return new DefaultModelClient(client, validateCacheTtl(cacheTtlMs));
246
+ }
247
+
248
+ /** @internal Create a model client with injectable auth host capabilities. */
249
+ export async function createModelClientWithDependencies(
250
+ options: ModelClientOptions,
251
+ dependencies: DatabricksAuthDependencies,
252
+ ): Promise<ModelClient> {
253
+ return new DefaultModelClient(
254
+ await DatabricksClient.create(options.auth, dependencies),
255
+ validateCacheTtl(options.cacheTtlMs),
256
+ );
257
+ }
258
+
259
+ function validateCacheTtl(value = DEFAULT_MODEL_CLIENT_CACHE_TTL_MS): number {
260
+ if (!Number.isFinite(value) || value <= 0) {
261
+ throw new Error("Model client cacheTtlMs must be a positive finite number");
262
+ }
263
+ return value;
264
+ }
265
+
266
+ function catalogueKey(client: DatabricksModelClient): string {
267
+ return [client.host(), client.workspaceId() ?? "", client.principal()].join("\u0000");
268
+ }
269
+
270
+ function inferProtocol(
271
+ endpoint: ServingEndpointSummary | undefined,
272
+ modelId: string,
273
+ ): ModelProtocol {
274
+ if (endpoint?.task === "llm/v1/embeddings") return "embeddings";
275
+ return modelServingApi(modelId);
276
+ }
277
+
278
+ function routeUrl(host: string, modelId: string, protocol: ModelProtocol): string {
279
+ if (protocol === "responses") return responsesUpstreamUrl(host, modelId);
280
+ if (protocol === "chat" && modelServingApi(modelId) === "responses") {
281
+ return chatCompletionsUrl(host);
282
+ }
283
+ return invocationsUrl(host, modelId);
284
+ }
285
+
286
+ function isUnmatchedExplicit(
287
+ catalogue: readonly ServingEndpointSummary[],
288
+ input: ResolveModelInput,
289
+ resolved: ResolvedModelSelection,
290
+ ): boolean {
291
+ return (
292
+ input.explicit !== undefined &&
293
+ input.fuzzy !== false &&
294
+ resolved.modelId === input.explicit &&
295
+ !catalogue.some((endpoint) => endpoint.name === input.explicit)
296
+ );
297
+ }
298
+
299
+ function cloneEndpoints(endpoints: readonly ServingEndpointSummary[]): ServingEndpointSummary[] {
300
+ return endpoints.map(cloneEndpoint);
301
+ }
302
+
303
+ function cloneEndpoint(endpoint: ServingEndpointSummary): ServingEndpointSummary {
304
+ return {
305
+ ...endpoint,
306
+ ...(endpoint.profile ? { profile: { ...endpoint.profile } } : {}),
307
+ ...(endpoint.serviceNames ? { serviceNames: { ...endpoint.serviceNames } } : {}),
308
+ ...(endpoint.reasoningEfforts ? { reasoningEfforts: [...endpoint.reasoningEfforts] } : {}),
309
+ ...(endpoint.status ? { status: { ...endpoint.status } } : {}),
310
+ };
311
+ }
package/src/policy.ts CHANGED
@@ -1,40 +1,197 @@
1
- /**
2
- * Browser-safe model metadata projected from the Rust policy bindings.
3
- *
4
- * @module
5
- */
1
+ /** TypeScript-owned model-family and serving capability policy. */
6
2
 
7
3
  import {
8
- modelFamily as modelFamilyWithRust,
9
- reasoningEffortNamesByFamily,
10
- supportsToolsByFamily as supportsToolsByFamilyWithRust,
11
- } from "@dbx-tools/model-rs";
12
- import {
13
- model,
14
- ReasoningEffortSchema,
4
+ ModelClass,
15
5
  type ReasoningEffort,
16
6
  type ServingEndpointSummary,
17
- } from "@dbx-tools/shared-model";
7
+ } from "@dbx-tools/shared-model/contracts";
8
+
9
+ import { supportsToolsByFamily } from "./classify.ts";
10
+
11
+ const FAMILIES = [
12
+ "bge",
13
+ "claude",
14
+ "deepseek",
15
+ "gemini",
16
+ "gemma",
17
+ "glm",
18
+ "gpt",
19
+ "grok",
20
+ "gte",
21
+ "inkling",
22
+ "kimi",
23
+ "llama",
24
+ "qwen",
25
+ ] as const;
18
26
 
19
- /** Return the normalized family parsed by the Rust model-name policy. */
27
+ const STANDARD: ReasoningEffort[] = ["low", "medium", "high"];
28
+ const GPT_5_6: ReasoningEffort[] = ["none", "low", "medium", "high", "xhigh", "max"];
29
+ const GPT_5_5_PRO: ReasoningEffort[] = ["medium", "high", "xhigh"];
30
+ const CLAUDE: ReasoningEffort[] = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
31
+ const GEMINI: ReasoningEffort[] = ["minimal", "low", "medium", "high"];
32
+
33
+ interface ParsedModelName {
34
+ family: (typeof FAMILIES)[number];
35
+ version: number[];
36
+ model: string[];
37
+ }
38
+
39
+ /** Return the normalized family parsed from a model identity. */
20
40
  export function modelFamily(name: string): string | undefined {
21
- return modelFamilyWithRust(name);
41
+ return parseModelName(name)?.family;
42
+ }
43
+
44
+ /** Normalize a recognized model identity into family, version, and variant tokens. */
45
+ export function modelSearchQuery(name: string): string | undefined {
46
+ const parsed = parseModelName(name);
47
+ if (!parsed) return undefined;
48
+ return [parsed.family, ...parsed.version.map(String), ...parsed.model].join(" ");
22
49
  }
23
50
 
24
51
  /** Return the reasoning efforts accepted by a model family. */
25
52
  export function modelReasoningEfforts(name: string): ReasoningEffort[] {
26
- return ReasoningEffortSchema.array().parse(reasoningEffortNamesByFamily(name));
53
+ const normalized = name.toLowerCase();
54
+ const parsed = parseModelName(name);
55
+ let efforts: ReasoningEffort[] = [];
56
+ if (parsed?.family === "gpt" && parsed.version.length > 0) {
57
+ const [major = 0, minor = 0] = parsed.version;
58
+ if (major >= 5) {
59
+ efforts =
60
+ major === 5 && minor === 5 && parsed.model.includes("pro")
61
+ ? GPT_5_5_PRO
62
+ : major === 5 && minor === 6
63
+ ? GPT_5_6
64
+ : STANDARD;
65
+ }
66
+ }
67
+ if (parsed?.family === "gpt" && parsed.model.includes("oss")) efforts = STANDARD;
68
+ if (parsed?.family === "claude") {
69
+ const [major = 0, minor = 0] = parsed.version;
70
+ if (major > 3 || (major === 3 && minor >= 7)) efforts = CLAUDE;
71
+ }
72
+ if (parsed?.family === "gemini") efforts = GEMINI;
73
+ if (normalized.includes("codex") || /(?:^|[-_./])o(?:1|3|4)(?:[-_./]|$)/i.test(name)) {
74
+ efforts = STANDARD;
75
+ }
76
+ return efforts;
27
77
  }
28
78
 
29
- /** Return whether Rust policy verifies a complete tool-calling round trip. */
79
+ /** Return whether policy verifies a complete tool-calling round trip. */
30
80
  export function modelSupportsTools(name: string): boolean {
31
- return supportsToolsByFamilyWithRust(name);
81
+ return supportsToolsByFamily(name);
32
82
  }
33
83
 
34
84
  /** Return whether a discovered endpoint can be used for a tool-calling chat. */
35
85
  export function endpointSupportsTools(endpoint: ServingEndpointSummary): boolean {
36
86
  const embedding =
37
- endpoint.task === "llm/v1/embeddings" || endpoint.class === model.ModelClass.Embedding;
87
+ endpoint.task === "llm/v1/embeddings" || endpoint.class === ModelClass.Embedding;
38
88
  const chat = !embedding && (endpoint.task === "llm/v1/chat" || endpoint.class !== undefined);
39
89
  return chat && (endpoint.supportsTools ?? modelSupportsTools(endpoint.name));
40
90
  }
91
+
92
+ /** Return whether a model requires Databricks' native Responses endpoint. */
93
+ export function isResponsesOnly(name: string): boolean {
94
+ if (name.toLowerCase().includes("codex")) return true;
95
+ const parsed = parseModelName(name);
96
+ if (!parsed || parsed.family !== "gpt" || parsed.model.includes("oss")) return false;
97
+ const [major = 0, minor = 0] = parsed.version;
98
+ return major > 5 || (major === 5 && minor >= 4);
99
+ }
100
+
101
+ /** Return the Databricks inference protocol required by a model. */
102
+ export function modelServingApi(name: string): "chat" | "responses" {
103
+ return isResponsesOnly(name) ? "responses" : "chat";
104
+ }
105
+
106
+ /** Return the Chat Completions effort required when function tools are present. */
107
+ export function chatToolReasoningEffort(name: string): ReasoningEffort | undefined {
108
+ const parsed = parseModelName(name);
109
+ if (!parsed || parsed.family !== "gpt") return undefined;
110
+ const [major = 0, minor = 0] = parsed.version;
111
+ return major === 5 && minor === 6 ? "none" : undefined;
112
+ }
113
+
114
+ /** Derive provider-specific model names from a Databricks model identity. */
115
+ export function modelServiceNames(name: string): Record<string, string> {
116
+ const parsed = parseModelName(name);
117
+ if (!parsed) return {};
118
+ const version = parsed.version.join(".");
119
+ const suffix = parsed.model.length ? `-${parsed.model.join("-")}` : "";
120
+ switch (parsed.family) {
121
+ case "gpt":
122
+ return parsed.model.includes("oss") ? {} : { openai: `gpt-${version}${suffix}` };
123
+ case "claude": {
124
+ const [variant, ...rest] = parsed.model;
125
+ return variant
126
+ ? { anthropic: `claude-${variant}-${version}${rest.length ? `-${rest.join("-")}` : ""}` }
127
+ : {};
128
+ }
129
+ case "gemini":
130
+ case "gemma":
131
+ return { google: `${parsed.family}-${version}${suffix}` };
132
+ case "glm":
133
+ return { zhipu: `glm-${version}${suffix}` };
134
+ case "grok":
135
+ return { xai: `grok-${version}${suffix}` };
136
+ case "llama":
137
+ return { meta: `llama-${version}${suffix}` };
138
+ case "qwen":
139
+ return { alibaba: `qwen${version}${suffix}` };
140
+ case "deepseek":
141
+ return { deepseek: `deepseek-v${parsed.version[0] ?? ""}${suffix}` };
142
+ case "kimi":
143
+ return { moonshot: `kimi-k${parsed.version[0] ?? ""}${suffix}` };
144
+ default:
145
+ return {};
146
+ }
147
+ }
148
+
149
+ function parseModelName(name: string): ParsedModelName | undefined {
150
+ const tokens = name.toLowerCase().match(/[a-z0-9]+/g) ?? [];
151
+ let familyIndex = -1;
152
+ let family: ParsedModelName["family"] | undefined;
153
+ for (let index = 0; index < tokens.length; index += 1) {
154
+ const token = tokens[index]!;
155
+ const compactQwen = /^qwen(\d+)$/.exec(token);
156
+ if (compactQwen) {
157
+ familyIndex = index;
158
+ family = "qwen";
159
+ break;
160
+ }
161
+ if (FAMILIES.includes(token as ParsedModelName["family"])) {
162
+ familyIndex = index;
163
+ family = token as ParsedModelName["family"];
164
+ break;
165
+ }
166
+ }
167
+ if (!family || familyIndex < 0) return undefined;
168
+ const remainder = tokens.slice(familyIndex + 1);
169
+ const numeric = (value: string) => /^\d+$/.test(value);
170
+ let versionStart = remainder.findIndex(numeric);
171
+ if (family === "qwen" && /^qwen\d+$/.test(tokens[familyIndex]!)) {
172
+ versionStart = 0;
173
+ remainder.unshift(tokens[familyIndex]!.slice(4));
174
+ }
175
+ if (family === "deepseek") {
176
+ const prefixed = remainder.findIndex((value) => /^v\d+$/.test(value));
177
+ if (prefixed >= 0) remainder[prefixed] = remainder[prefixed]!.slice(1);
178
+ versionStart = remainder.findIndex(numeric);
179
+ }
180
+ if (family === "kimi") {
181
+ const prefixed = remainder.findIndex((value) => /^k\d+$/.test(value));
182
+ if (prefixed >= 0) remainder[prefixed] = remainder[prefixed]!.slice(1);
183
+ versionStart = remainder.findIndex(numeric);
184
+ }
185
+ if (versionStart < 0) return { family, version: [], model: remainder };
186
+ const version: number[] = [];
187
+ let cursor = versionStart;
188
+ while (cursor < remainder.length && version.length < 2 && numeric(remainder[cursor]!)) {
189
+ version.push(Number(remainder[cursor]));
190
+ cursor += 1;
191
+ }
192
+ return {
193
+ family,
194
+ version,
195
+ model: [...remainder.slice(0, versionStart), ...remainder.slice(cursor)],
196
+ };
197
+ }
package/src/python.ts ADDED
@@ -0,0 +1,2 @@
1
+ /** Portable entry point used by generated Python bindings. */
2
+ export { createModelClient } from "./model-client.ts";