dbx-tools-model 0.6.78__tar.gz

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.
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.3
2
+ Name: dbx-tools-model
3
+ Version: 0.6.78
4
+ Summary: Databricks Model Serving invocation, classification, and endpoint resolution
5
+ Requires-Dist: databricks-sdk>=0.63.0,<1
6
+ Requires-Dist: pydantic>=2.9,<3
7
+ Requires-Python: >=3.10
8
+ Project-URL: Source, https://github.com/reggie-db/dbx-tools/tree/main/packages/py/model
9
+ Description-Content-Type: text/markdown
10
+
11
+ # `dbx-tools-model`
12
+
13
+ Python contracts and runtime helpers for Databricks Model Serving. This package
14
+ mirrors the reusable parts of `@dbx-tools/shared-model` and `@dbx-tools/model`
15
+ without AppKit cache or Mastra dependencies.
16
+
17
+ Install directly from this monorepo:
18
+
19
+ ```bash
20
+ pip install "dbx-tools-model @ git+https://github.com/reggie-db/dbx-tools.git@main#subdirectory=packages/py/model"
21
+ ```
22
+
23
+ Key features:
24
+
25
+ - stable Pydantic endpoint, profile, query, and ranked-result models;
26
+ - live endpoint listing through a structural `WorkspaceClient` protocol;
27
+ - score-driven model classification with family fallbacks;
28
+ - exact and fuzzy endpoint resolution with deterministic class ordering;
29
+ - Databricks invocation URL and per-request authentication helpers;
30
+ - OpenAI chat request sanitization and content extraction;
31
+ - embedding vector extraction with optional dimension validation.
32
+
33
+ ```python
34
+ from databricks.sdk import WorkspaceClient
35
+ from dbx_tools.model import ModelClass, list_serving_endpoints, resolve_model
36
+
37
+ endpoints = list_serving_endpoints(WorkspaceClient())
38
+ selection = resolve_model(endpoints, model_class=ModelClass.CHAT_BALANCED)
39
+ print(selection.model_id)
40
+ ```
41
+
42
+ The Python port intentionally omits AppKit `CacheManager` integration,
43
+ Mastra-specific adapters, and browser-only schemas. Callers can cache the plain
44
+ Pydantic results with their preferred Python cache.
45
+
46
+ ## Relationship to the Databricks SDK
47
+
48
+ Use the native SDK directly when an endpoint name is already known and its typed
49
+ query method fits the request. Use this package when endpoint choice, stable
50
+ cross-runtime models, OpenAI-shaped HTTP invocation, or provider-neutral chat
51
+ and embedding normalization is the repetitive part.
52
+
53
+ ## Module map
54
+
55
+ - `models` — Pydantic wire contracts;
56
+ - `classify`, `classes`, `fallback` — model taxonomy and ordering;
57
+ - `resolve` — exact/fuzzy ranking and single-model selection;
58
+ - `serving` — structural `WorkspaceClient` endpoint listing;
59
+ - `invoke` — URLs, SDK authentication headers, and JSON POST helpers;
60
+ - `chat`, `embedding` — request sanitization and response extraction.
@@ -0,0 +1,50 @@
1
+ # `dbx-tools-model`
2
+
3
+ Python contracts and runtime helpers for Databricks Model Serving. This package
4
+ mirrors the reusable parts of `@dbx-tools/shared-model` and `@dbx-tools/model`
5
+ without AppKit cache or Mastra dependencies.
6
+
7
+ Install directly from this monorepo:
8
+
9
+ ```bash
10
+ pip install "dbx-tools-model @ git+https://github.com/reggie-db/dbx-tools.git@main#subdirectory=packages/py/model"
11
+ ```
12
+
13
+ Key features:
14
+
15
+ - stable Pydantic endpoint, profile, query, and ranked-result models;
16
+ - live endpoint listing through a structural `WorkspaceClient` protocol;
17
+ - score-driven model classification with family fallbacks;
18
+ - exact and fuzzy endpoint resolution with deterministic class ordering;
19
+ - Databricks invocation URL and per-request authentication helpers;
20
+ - OpenAI chat request sanitization and content extraction;
21
+ - embedding vector extraction with optional dimension validation.
22
+
23
+ ```python
24
+ from databricks.sdk import WorkspaceClient
25
+ from dbx_tools.model import ModelClass, list_serving_endpoints, resolve_model
26
+
27
+ endpoints = list_serving_endpoints(WorkspaceClient())
28
+ selection = resolve_model(endpoints, model_class=ModelClass.CHAT_BALANCED)
29
+ print(selection.model_id)
30
+ ```
31
+
32
+ The Python port intentionally omits AppKit `CacheManager` integration,
33
+ Mastra-specific adapters, and browser-only schemas. Callers can cache the plain
34
+ Pydantic results with their preferred Python cache.
35
+
36
+ ## Relationship to the Databricks SDK
37
+
38
+ Use the native SDK directly when an endpoint name is already known and its typed
39
+ query method fits the request. Use this package when endpoint choice, stable
40
+ cross-runtime models, OpenAI-shaped HTTP invocation, or provider-neutral chat
41
+ and embedding normalization is the repetitive part.
42
+
43
+ ## Module map
44
+
45
+ - `models` — Pydantic wire contracts;
46
+ - `classify`, `classes`, `fallback` — model taxonomy and ordering;
47
+ - `resolve` — exact/fuzzy ranking and single-model selection;
48
+ - `serving` — structural `WorkspaceClient` endpoint listing;
49
+ - `invoke` — URLs, SDK authentication headers, and JSON POST helpers;
50
+ - `chat`, `embedding` — request sanitization and response extraction.
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "dbx-tools-model"
3
+ version = "0.6.78"
4
+ description = "Databricks Model Serving invocation, classification, and endpoint resolution"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "databricks-sdk>=0.63.0,<1",
9
+ "pydantic>=2.9,<3",
10
+ ]
11
+
12
+ [project.urls]
13
+ Source = "https://github.com/reggie-db/dbx-tools/tree/main/packages/py/model"
14
+
15
+ [build-system]
16
+ requires = ["uv_build>=0.11.28,<0.12.0"]
17
+ build-backend = "uv_build"
18
+
19
+ [tool.uv.build-backend]
20
+ module-name = "dbx_tools.model"
21
+ module-root = "src"
22
+ namespace = true
@@ -0,0 +1,21 @@
1
+ # ~~ Generated by projen. To modify, edit .projenrc.js and run "bunx projen".
2
+
3
+ [project]
4
+ name = "dbx-tools-model"
5
+ version = "0.6.78"
6
+ description = "Databricks Model Serving invocation, classification, and endpoint resolution"
7
+ readme = "README.md"
8
+ requires-python = ">=3.10"
9
+ dependencies = [ "databricks-sdk>=0.63.0,<1", "pydantic>=2.9,<3" ]
10
+
11
+ [project.urls]
12
+ Source = "https://github.com/reggie-db/dbx-tools/tree/main/packages/py/model"
13
+
14
+ [build-system]
15
+ requires = [ "uv_build>=0.11.28,<0.12.0" ]
16
+ build-backend = "uv_build"
17
+
18
+ [tool.uv.build-backend]
19
+ module-name = "dbx_tools.model"
20
+ module-root = "src"
21
+ namespace = true
@@ -0,0 +1,113 @@
1
+ from .chat import (
2
+ UNSUPPORTED_CHAT_FIELDS,
3
+ chat_content_to_text,
4
+ sanitize_chat_request,
5
+ strip_unsupported_chat_fields,
6
+ )
7
+ from .classes import (
8
+ CHAT_CLASS_ORDER,
9
+ MODEL_CLASS_ORDER,
10
+ classes_at_or_below,
11
+ is_chat_class,
12
+ parse_model_class,
13
+ )
14
+ from .classify import (
15
+ CHAT_TASK,
16
+ EMBEDDING_TASK,
17
+ classify_by_family,
18
+ classify_endpoints,
19
+ endpoint_capabilities,
20
+ supports_tools_by_family,
21
+ version_tuple,
22
+ )
23
+ from .embedding import embedding_dimension, extract_embedding, extract_embeddings
24
+ from .fallback import FALLBACK_MODEL_IDS, model_for_class, models_for_class
25
+ from .invoke import (
26
+ CHAT_COMPLETIONS_PATH,
27
+ INVOCATIONS_SUFFIX,
28
+ OPEN_RESPONSES_PATH,
29
+ RESPONSES_PATH,
30
+ auth_headers,
31
+ chat_completions_url,
32
+ invocations_url,
33
+ invoke_json,
34
+ is_responses_only,
35
+ open_responses_url,
36
+ post_json,
37
+ responses_upstream_url,
38
+ responses_url,
39
+ )
40
+ from .models import (
41
+ EndpointCapabilities,
42
+ ModelClass,
43
+ ModelProfile,
44
+ ModelQuery,
45
+ RankedModel,
46
+ ResolvedModel,
47
+ ResolvedModelSelection,
48
+ ServingEndpointSummary,
49
+ )
50
+ from .resolve import (
51
+ DEFAULT_FUZZY_THRESHOLD,
52
+ rank_model_id,
53
+ rank_models,
54
+ resolve_model,
55
+ resolve_model_id,
56
+ search_serving_endpoints,
57
+ )
58
+ from .serving import list_serving_endpoints, list_serving_endpoints_uncached, to_model_display_name
59
+
60
+ __all__ = [
61
+ "CHAT_CLASS_ORDER",
62
+ "CHAT_COMPLETIONS_PATH",
63
+ "CHAT_TASK",
64
+ "DEFAULT_FUZZY_THRESHOLD",
65
+ "EMBEDDING_TASK",
66
+ "FALLBACK_MODEL_IDS",
67
+ "INVOCATIONS_SUFFIX",
68
+ "MODEL_CLASS_ORDER",
69
+ "OPEN_RESPONSES_PATH",
70
+ "RESPONSES_PATH",
71
+ "UNSUPPORTED_CHAT_FIELDS",
72
+ "EndpointCapabilities",
73
+ "ModelClass",
74
+ "ModelProfile",
75
+ "ModelQuery",
76
+ "RankedModel",
77
+ "ResolvedModel",
78
+ "ResolvedModelSelection",
79
+ "ServingEndpointSummary",
80
+ "auth_headers",
81
+ "chat_completions_url",
82
+ "chat_content_to_text",
83
+ "classes_at_or_below",
84
+ "classify_by_family",
85
+ "classify_endpoints",
86
+ "embedding_dimension",
87
+ "endpoint_capabilities",
88
+ "extract_embedding",
89
+ "extract_embeddings",
90
+ "invocations_url",
91
+ "invoke_json",
92
+ "is_chat_class",
93
+ "is_responses_only",
94
+ "list_serving_endpoints",
95
+ "list_serving_endpoints_uncached",
96
+ "model_for_class",
97
+ "models_for_class",
98
+ "open_responses_url",
99
+ "parse_model_class",
100
+ "post_json",
101
+ "rank_model_id",
102
+ "rank_models",
103
+ "resolve_model",
104
+ "resolve_model_id",
105
+ "responses_upstream_url",
106
+ "responses_url",
107
+ "sanitize_chat_request",
108
+ "search_serving_endpoints",
109
+ "strip_unsupported_chat_fields",
110
+ "supports_tools_by_family",
111
+ "to_model_display_name",
112
+ "version_tuple",
113
+ ]
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+ UNSUPPORTED_CHAT_FIELDS = (
6
+ "parallel_tool_calls",
7
+ "store",
8
+ "metadata",
9
+ "service_tier",
10
+ "prompt_cache_key",
11
+ "safety_identifier",
12
+ )
13
+
14
+
15
+ def strip_unsupported_chat_fields(body: dict[str, object], extra: Iterable[str] = ()) -> list[str]:
16
+ dropped = []
17
+ for field in (*UNSUPPORTED_CHAT_FIELDS, *extra):
18
+ if field in body:
19
+ del body[field]
20
+ dropped.append(field)
21
+ return dropped
22
+
23
+
24
+ def sanitize_chat_request(body: dict[str, object], extra: Iterable[str] = ()) -> dict[str, object]:
25
+ sanitized = dict(body)
26
+ strip_unsupported_chat_fields(sanitized, extra)
27
+ return sanitized
28
+
29
+
30
+ def chat_content_to_text(
31
+ content: object,
32
+ options: dict[str, object] | None = None,
33
+ *,
34
+ separator: str = "",
35
+ types: Iterable[str] | None = None,
36
+ ) -> str:
37
+ if options:
38
+ separator = str(options.get("separator", separator))
39
+ option_types = options.get("types")
40
+ if isinstance(option_types, list) and all(isinstance(value, str) for value in option_types):
41
+ types = option_types
42
+ if isinstance(content, str):
43
+ return content
44
+ if not isinstance(content, list):
45
+ return ""
46
+ accepted = set(types) if types is not None else None
47
+ parts = []
48
+ for part in content:
49
+ if not isinstance(part, dict):
50
+ continue
51
+ if accepted is not None and part.get("type") not in accepted:
52
+ continue
53
+ text = part.get("text")
54
+ if isinstance(text, str):
55
+ parts.append(text)
56
+ return separator.join(parts)
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from .models import ModelClass
4
+
5
+ CHAT_CLASS_ORDER = (
6
+ ModelClass.CHAT_THINKING,
7
+ ModelClass.CHAT_BALANCED,
8
+ ModelClass.CHAT_FAST,
9
+ )
10
+ MODEL_CLASS_ORDER = (*CHAT_CLASS_ORDER, ModelClass.EMBEDDING)
11
+
12
+
13
+ def is_chat_class(model_class: ModelClass) -> bool:
14
+ return model_class in CHAT_CLASS_ORDER
15
+
16
+
17
+ def parse_model_class(value: object) -> ModelClass | None:
18
+ try:
19
+ return ModelClass(value)
20
+ except (TypeError, ValueError):
21
+ try:
22
+ return ModelClass(f"chat-{value}")
23
+ except (TypeError, ValueError):
24
+ return None
25
+
26
+
27
+ def classes_at_or_below(model_class: ModelClass) -> list[ModelClass]:
28
+ if model_class == ModelClass.EMBEDDING:
29
+ return [ModelClass.EMBEDDING]
30
+ try:
31
+ return list(CHAT_CLASS_ORDER[CHAT_CLASS_ORDER.index(model_class) :])
32
+ except ValueError:
33
+ return list(CHAT_CLASS_ORDER)
@@ -0,0 +1,181 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import re
5
+ from collections.abc import Iterable
6
+
7
+ from .models import EndpointCapabilities, ModelClass, ServingEndpointSummary
8
+
9
+ CHAT_TASK = "llm/v1/chat"
10
+ EMBEDDING_TASK = "llm/v1/embeddings"
11
+
12
+
13
+ def supports_tools_by_family(name: str) -> bool:
14
+ normalized = name.lower()
15
+ if "gemini" in normalized or "gpt-oss" in normalized:
16
+ return False
17
+ return any(family in normalized for family in ("claude", "gpt", "qwen", "glm", "llama"))
18
+
19
+
20
+ def endpoint_capabilities(
21
+ endpoint: ServingEndpointSummary | dict[str, object],
22
+ ) -> EndpointCapabilities:
23
+ summary = _summary(endpoint)
24
+ embedding = summary.task == EMBEDDING_TASK or summary.model_class == ModelClass.EMBEDDING
25
+ chat = not embedding and (summary.task == CHAT_TASK or summary.model_class is not None)
26
+ tools = chat and (
27
+ summary.supports_tools
28
+ if summary.supports_tools is not None
29
+ else supports_tools_by_family(summary.name)
30
+ )
31
+ return EndpointCapabilities(chat=chat, embedding=embedding, tools=tools)
32
+
33
+
34
+ def version_tuple(name: str) -> list[int]:
35
+ match = re.search(r"\d", name)
36
+ if match is None:
37
+ return [0, 0, 0]
38
+ numbers = []
39
+ for chunk in re.split(r"[^a-z0-9]+", name[match.start() :], flags=re.IGNORECASE):
40
+ digits = re.match(r"^\d+", chunk)
41
+ if digits:
42
+ numbers.append(int(digits.group(0)))
43
+ return (numbers + [0, 0, 0])[:3]
44
+
45
+
46
+ def classify_by_family(name: str) -> dict[str, object] | None:
47
+ normalized = name.lower()
48
+
49
+ def result(model_class: ModelClass) -> dict[str, object]:
50
+ major, minor, patch = version_tuple(normalized)
51
+ return {"class": model_class.value, "rank": major * 1_000_000 + minor * 1_000 + patch}
52
+
53
+ if "opus" in normalized:
54
+ return result(ModelClass.CHAT_THINKING)
55
+ if "sonnet" in normalized:
56
+ return result(ModelClass.CHAT_BALANCED)
57
+ if "haiku" in normalized:
58
+ return result(ModelClass.CHAT_FAST)
59
+ if "gpt-oss" in normalized:
60
+ return result(ModelClass.CHAT_BALANCED if "120b" in normalized else ModelClass.CHAT_FAST)
61
+ if "gpt" in normalized:
62
+ if "pro" in normalized:
63
+ return result(ModelClass.CHAT_THINKING)
64
+ if "mini" in normalized or "nano" in normalized:
65
+ return result(ModelClass.CHAT_FAST)
66
+ return result(ModelClass.CHAT_BALANCED)
67
+ if "gemini" in normalized:
68
+ if "flash-lite" in normalized:
69
+ return result(ModelClass.CHAT_FAST)
70
+ if "pro" in normalized:
71
+ return result(ModelClass.CHAT_THINKING)
72
+ return result(ModelClass.CHAT_BALANCED)
73
+ if "gemma" in normalized:
74
+ return result(ModelClass.CHAT_FAST)
75
+ if "llama" in normalized:
76
+ if "maverick" in normalized or "405b" in normalized:
77
+ return result(ModelClass.CHAT_THINKING)
78
+ if "70b" in normalized:
79
+ return result(ModelClass.CHAT_BALANCED)
80
+ if "8b" in normalized or "1b" in normalized:
81
+ return result(ModelClass.CHAT_FAST)
82
+ return result(ModelClass.CHAT_BALANCED)
83
+ if "qwen" in normalized:
84
+ return result(ModelClass.CHAT_BALANCED)
85
+ return None
86
+
87
+
88
+ def classify_endpoints(
89
+ endpoints: Iterable[ServingEndpointSummary | dict[str, object]],
90
+ ) -> dict[str, list[dict[str, object]]]:
91
+ summaries = [_summary(endpoint) for endpoint in endpoints]
92
+ chat = [endpoint for endpoint in summaries if endpoint.task == CHAT_TASK]
93
+ qualities = sorted(
94
+ endpoint.profile.quality
95
+ for endpoint in chat
96
+ if endpoint.profile is not None
97
+ and endpoint.profile.quality is not None
98
+ and math.isfinite(endpoint.profile.quality)
99
+ )
100
+ low = _quantile(qualities, 1 / 3)
101
+ high = _quantile(qualities, 2 / 3)
102
+ buckets: dict[ModelClass, list[tuple[tuple[object, ...], ServingEndpointSummary]]] = {
103
+ model_class: [] for model_class in ModelClass
104
+ }
105
+
106
+ for endpoint in chat:
107
+ quality = endpoint.profile.quality if endpoint.profile else None
108
+ if quality is not None and math.isfinite(quality):
109
+ model_class = (
110
+ ModelClass.CHAT_THINKING
111
+ if quality >= high
112
+ else ModelClass.CHAT_FAST
113
+ if quality <= low
114
+ else ModelClass.CHAT_BALANCED
115
+ )
116
+ cost = (
117
+ endpoint.profile.cost
118
+ if endpoint.profile and endpoint.profile.cost is not None
119
+ else math.inf
120
+ )
121
+ speed = (
122
+ endpoint.profile.speed
123
+ if endpoint.profile and endpoint.profile.speed is not None
124
+ else 0
125
+ )
126
+ key = (0, -quality, cost, -speed, *[-part for part in version_tuple(endpoint.name)])
127
+ else:
128
+ family = classify_by_family(endpoint.name)
129
+ if family is None:
130
+ continue
131
+ model_class = ModelClass(str(family["class"]))
132
+ key = (
133
+ 1,
134
+ -int(family["rank"]),
135
+ math.inf,
136
+ 0,
137
+ *[-part for part in version_tuple(endpoint.name)],
138
+ )
139
+ buckets[model_class].append((key, endpoint))
140
+
141
+ embeddings = [endpoint for endpoint in summaries if endpoint.task == EMBEDDING_TASK]
142
+ return {
143
+ ModelClass.CHAT_THINKING.value: _dump_sorted(buckets[ModelClass.CHAT_THINKING]),
144
+ ModelClass.CHAT_BALANCED.value: _dump_sorted(buckets[ModelClass.CHAT_BALANCED]),
145
+ ModelClass.CHAT_FAST.value: _dump_sorted(buckets[ModelClass.CHAT_FAST]),
146
+ ModelClass.EMBEDDING.value: [endpoint.as_dict() for endpoint in embeddings],
147
+ }
148
+
149
+
150
+ def classified_summaries(
151
+ endpoints: Iterable[ServingEndpointSummary | dict[str, object]],
152
+ ) -> dict[ModelClass, list[ServingEndpointSummary]]:
153
+ return {
154
+ ModelClass(key): [ServingEndpointSummary.model_validate(value) for value in values]
155
+ for key, values in classify_endpoints(endpoints).items()
156
+ }
157
+
158
+
159
+ def _summary(value: ServingEndpointSummary | dict[str, object]) -> ServingEndpointSummary:
160
+ return (
161
+ value
162
+ if isinstance(value, ServingEndpointSummary)
163
+ else ServingEndpointSummary.model_validate(value)
164
+ )
165
+
166
+
167
+ def _quantile(values: list[float], probability: float) -> float:
168
+ if not values:
169
+ return math.nan
170
+ index = (len(values) - 1) * probability
171
+ lower = math.floor(index)
172
+ upper = math.ceil(index)
173
+ if lower == upper:
174
+ return values[lower]
175
+ return values[lower] + (values[upper] - values[lower]) * (index - lower)
176
+
177
+
178
+ def _dump_sorted(
179
+ values: list[tuple[tuple[object, ...], ServingEndpointSummary]],
180
+ ) -> list[dict[str, object]]:
181
+ return [endpoint.as_dict() for _, endpoint in sorted(values, key=lambda value: value[0])]
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+
6
+ def extract_embedding(response: object, expected_dimension: int | None = None) -> list[float]:
7
+ if not isinstance(response, dict):
8
+ raise TypeError("Embedding response must be an object")
9
+ data = response.get("data")
10
+ if not isinstance(data, list) or not data or not isinstance(data[0], dict):
11
+ raise ValueError("Embedding response contains no data")
12
+ vector = data[0].get("embedding")
13
+ if not isinstance(vector, list) or not all(isinstance(value, (int, float)) for value in vector):
14
+ raise ValueError("Embedding response contains no numeric vector")
15
+ result = [float(value) for value in vector]
16
+ if expected_dimension is not None and len(result) != expected_dimension:
17
+ raise ValueError(
18
+ f"Expected embedding dimension {expected_dimension}, received {len(result)}"
19
+ )
20
+ return result
21
+
22
+
23
+ def extract_embeddings(
24
+ response: object, expected_dimension: int | None = None
25
+ ) -> list[list[float]]:
26
+ if not isinstance(response, dict) or not isinstance(response.get("data"), list):
27
+ raise TypeError("Embedding response contains no data")
28
+ return [
29
+ extract_embedding({"data": [item]}, expected_dimension)
30
+ for item in response["data"]
31
+ if isinstance(item, dict)
32
+ ]
33
+
34
+
35
+ def embedding_dimension(vector: Iterable[float]) -> int:
36
+ return len(list(vector))
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from .classify import classify_by_family
4
+ from .models import ModelClass
5
+
6
+ _FALLBACK_MODEL_NAMES = (
7
+ "databricks-claude-opus-4-8",
8
+ "databricks-gpt-5-5-pro",
9
+ "databricks-gemini-3-1-pro",
10
+ "databricks-claude-sonnet-4-6",
11
+ "databricks-gpt-5-5",
12
+ "databricks-meta-llama-3-3-70b-instruct",
13
+ "databricks-claude-haiku-4-5",
14
+ "databricks-gpt-5-nano",
15
+ "databricks-meta-llama-3-1-8b-instruct",
16
+ )
17
+
18
+
19
+ def models_for_class(model_class: ModelClass) -> list[str]:
20
+ classified = []
21
+ for name in _FALLBACK_MODEL_NAMES:
22
+ family = classify_by_family(name)
23
+ if family is not None and family["class"] == model_class.value:
24
+ classified.append((int(family["rank"]), name))
25
+ return [name for _, name in sorted(classified, reverse=True)]
26
+
27
+
28
+ def model_for_class(model_class: ModelClass) -> str:
29
+ return models_for_class(model_class)[0]
30
+
31
+
32
+ FALLBACK_MODEL_IDS = tuple(
33
+ model
34
+ for model_class in (
35
+ ModelClass.CHAT_THINKING,
36
+ ModelClass.CHAT_BALANCED,
37
+ ModelClass.CHAT_FAST,
38
+ )
39
+ for model in models_for_class(model_class)
40
+ )
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from collections.abc import Callable, Mapping
6
+ from typing import Any, Protocol
7
+ from urllib.parse import quote, urljoin
8
+ from urllib.request import Request, urlopen
9
+
10
+ INVOCATIONS_SUFFIX = "invocations"
11
+ RESPONSES_PATH = "serving-endpoints/responses"
12
+ OPEN_RESPONSES_PATH = "serving-endpoints/open-responses"
13
+ CHAT_COMPLETIONS_PATH = "serving-endpoints/chat/completions"
14
+
15
+
16
+ class AuthenticatingConfig(Protocol):
17
+ def authenticate(self) -> Mapping[str, str]: ...
18
+
19
+
20
+ class AuthenticatingClientLike(Protocol):
21
+ config: AuthenticatingConfig
22
+
23
+
24
+ def invocations_url(host: str, endpoint: str) -> str:
25
+ return urljoin(host, f"serving-endpoints/{quote(endpoint, safe='')}/{INVOCATIONS_SUFFIX}")
26
+
27
+
28
+ def responses_url(host: str) -> str:
29
+ return urljoin(host, RESPONSES_PATH)
30
+
31
+
32
+ def open_responses_url(host: str) -> str:
33
+ return urljoin(host, OPEN_RESPONSES_PATH)
34
+
35
+
36
+ def chat_completions_url(host: str) -> str:
37
+ return urljoin(host, CHAT_COMPLETIONS_PATH)
38
+
39
+
40
+ def is_responses_only(endpoint: str) -> bool:
41
+ return "codex" in endpoint.lower()
42
+
43
+
44
+ def responses_upstream_url(host: str, endpoint: str) -> str:
45
+ normalized = endpoint.lower()
46
+ openai_family = (
47
+ "gpt" in normalized
48
+ or "codex" in normalized
49
+ or re.search(r"(^|[^a-z])o[1-9]([^a-z]|$)", normalized) is not None
50
+ or "openai" in normalized
51
+ )
52
+ return responses_url(host) if openai_family else open_responses_url(host)
53
+
54
+
55
+ def auth_headers(client: AuthenticatingClientLike) -> dict[str, str]:
56
+ return dict(client.config.authenticate())
57
+
58
+
59
+ def post_json(
60
+ client: AuthenticatingClientLike,
61
+ url: str,
62
+ body: Mapping[str, object],
63
+ *,
64
+ timeout: float | None = None,
65
+ opener: Callable[..., Any] = urlopen,
66
+ ) -> object:
67
+ headers = {"Accept": "application/json", "Content-Type": "application/json"}
68
+ headers.update(auth_headers(client))
69
+ request = Request(url, data=json.dumps(body).encode(), headers=headers, method="POST")
70
+ with opener(request, timeout=timeout) as response:
71
+ return json.loads(response.read())
72
+
73
+
74
+ def invoke_json(
75
+ client: AuthenticatingClientLike,
76
+ host: str,
77
+ endpoint: str,
78
+ body: Mapping[str, object],
79
+ **options: object,
80
+ ) -> object:
81
+ return post_json(client, invocations_url(host, endpoint), body, **options)
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ class ModelClass(str, Enum):
9
+ CHAT_THINKING = "chat-thinking"
10
+ CHAT_BALANCED = "chat-balanced"
11
+ CHAT_FAST = "chat-fast"
12
+ EMBEDDING = "embedding"
13
+
14
+
15
+ class WireModel(BaseModel):
16
+ model_config = ConfigDict(populate_by_name=True, extra="ignore")
17
+
18
+ def as_dict(self) -> dict[str, object]:
19
+ return self.model_dump(by_alias=True, exclude_none=True, mode="json")
20
+
21
+
22
+ class ModelProfile(WireModel):
23
+ quality: float | None = None
24
+ speed: float | None = None
25
+ cost: float | None = None
26
+
27
+
28
+ class ServingEndpointSummary(WireModel):
29
+ name: str
30
+ display_name: str | None = Field(default=None, alias="displayName")
31
+ task: str | None = None
32
+ state: str | None = None
33
+ description: str | None = None
34
+ supports_tools: bool | None = Field(default=None, alias="supportsTools")
35
+ profile: ModelProfile | None = None
36
+ model_class: ModelClass | None = Field(default=None, alias="class")
37
+ dimension: int | None = None
38
+
39
+
40
+ class ModelQuery(WireModel):
41
+ search: str | None = None
42
+ model_class: ModelClass | None = Field(default=None, alias="modelClass")
43
+ requires_tools: bool | None = Field(default=None, alias="requiresTools")
44
+ limit: int | None = None
45
+ threshold: float | None = None
46
+
47
+
48
+ class RankedModel(WireModel):
49
+ endpoint: ServingEndpointSummary
50
+ model_class: ModelClass = Field(alias="modelClass")
51
+ score: float | None = None
52
+
53
+
54
+ class ResolvedModel(WireModel):
55
+ model_id: str = Field(alias="modelId")
56
+ matched: bool
57
+ score: float | None = None
58
+
59
+
60
+ class ResolvedModelSelection(WireModel):
61
+ model_id: str = Field(alias="modelId")
62
+ source: str
63
+
64
+
65
+ class EndpointCapabilities(WireModel):
66
+ chat: bool
67
+ embedding: bool
68
+ tools: bool
@@ -0,0 +1,218 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from collections.abc import Iterable, Sequence
5
+ from difflib import SequenceMatcher
6
+
7
+ from .classes import CHAT_CLASS_ORDER, MODEL_CLASS_ORDER, classes_at_or_below
8
+ from .classify import classified_summaries, endpoint_capabilities
9
+ from .fallback import FALLBACK_MODEL_IDS, models_for_class
10
+ from .models import (
11
+ ModelClass,
12
+ ModelQuery,
13
+ RankedModel,
14
+ ResolvedModel,
15
+ ResolvedModelSelection,
16
+ ServingEndpointSummary,
17
+ )
18
+
19
+ DEFAULT_FUZZY_THRESHOLD = 0.4
20
+
21
+
22
+ def search_serving_endpoints(
23
+ input_value: str,
24
+ endpoints: Iterable[ServingEndpointSummary | dict[str, object]],
25
+ threshold: float = DEFAULT_FUZZY_THRESHOLD,
26
+ ) -> list[dict[str, object]]:
27
+ summaries = [_summary(endpoint) for endpoint in endpoints]
28
+ for endpoint in summaries:
29
+ if endpoint.name == input_value:
30
+ return [{"endpoint": endpoint.as_dict(), "score": 0}]
31
+ tokens = re.findall(r"[a-zA-Z0-9]+", input_value.lower())
32
+ if not tokens:
33
+ return []
34
+ matches = []
35
+ for endpoint in summaries:
36
+ name = endpoint.name.lower()
37
+ token_scores = [_token_distance(token, name) for token in tokens]
38
+ score = sum(token_scores) / len(token_scores)
39
+ if score <= threshold:
40
+ matches.append({"endpoint": endpoint.as_dict(), "score": score})
41
+ return sorted(matches, key=lambda match: (float(match["score"]), match["endpoint"]["name"]))
42
+
43
+
44
+ def resolve_model_id(
45
+ input_value: str,
46
+ endpoints: Iterable[ServingEndpointSummary | dict[str, object]],
47
+ *,
48
+ threshold: float = DEFAULT_FUZZY_THRESHOLD,
49
+ requires_tools: bool = False,
50
+ ) -> ResolvedModel:
51
+ candidates = [
52
+ _summary(endpoint)
53
+ for endpoint in endpoints
54
+ if not requires_tools or endpoint_capabilities(_summary(endpoint)).tools
55
+ ]
56
+ matches = search_serving_endpoints(input_value, candidates, threshold)
57
+ if not matches:
58
+ return ResolvedModel(modelId=input_value, matched=False)
59
+ top = matches[0]
60
+ return ResolvedModel(
61
+ modelId=str(top["endpoint"]["name"]), matched=True, score=float(top["score"])
62
+ )
63
+
64
+
65
+ def rank_models(
66
+ endpoints: Iterable[ServingEndpointSummary | dict[str, object]],
67
+ query: ModelQuery | dict[str, object] | None = None,
68
+ ) -> list[dict[str, object]]:
69
+ summaries = [_summary(endpoint) for endpoint in endpoints]
70
+ request = query if isinstance(query, ModelQuery) else ModelQuery.model_validate(query or {})
71
+ classified = classified_summaries(summaries)
72
+ eligible = (
73
+ classes_at_or_below(request.model_class)
74
+ if request.model_class is not None
75
+ else list(CHAT_CLASS_ORDER)
76
+ )
77
+ candidates: list[RankedModel] = []
78
+ for model_class in eligible:
79
+ for endpoint in classified[model_class]:
80
+ if request.requires_tools and not endpoint_capabilities(endpoint).tools:
81
+ continue
82
+ candidates.append(RankedModel(endpoint=endpoint, modelClass=model_class))
83
+
84
+ search = request.search.strip() if request.search else ""
85
+ if search:
86
+ scores = {
87
+ str(match["endpoint"]["name"]): float(match["score"])
88
+ for match in search_serving_endpoints(
89
+ search,
90
+ [candidate.endpoint for candidate in candidates],
91
+ request.threshold if request.threshold is not None else DEFAULT_FUZZY_THRESHOLD,
92
+ )
93
+ }
94
+ candidates = [candidate for candidate in candidates if candidate.endpoint.name in scores]
95
+ for candidate in candidates:
96
+ candidate.score = scores[candidate.endpoint.name]
97
+ candidates.sort(
98
+ key=lambda candidate: (
99
+ round((candidate.score or 0) * 1000),
100
+ MODEL_CLASS_ORDER.index(candidate.model_class),
101
+ )
102
+ )
103
+ if request.limit is not None:
104
+ candidates = candidates[: max(0, request.limit)]
105
+ return [candidate.as_dict() for candidate in candidates]
106
+
107
+
108
+ def rank_model_id(
109
+ endpoints: Iterable[ServingEndpointSummary | dict[str, object]],
110
+ search: str,
111
+ *,
112
+ threshold: float = DEFAULT_FUZZY_THRESHOLD,
113
+ requires_tools: bool = False,
114
+ ) -> ResolvedModel:
115
+ ranked = rank_models(
116
+ endpoints,
117
+ ModelQuery(
118
+ search=search,
119
+ limit=1,
120
+ threshold=threshold,
121
+ requiresTools=requires_tools,
122
+ ),
123
+ )
124
+ if not ranked:
125
+ return ResolvedModel(modelId=search, matched=False)
126
+ return ResolvedModel(
127
+ modelId=str(ranked[0]["endpoint"]["name"]),
128
+ matched=True,
129
+ score=float(ranked[0]["score"]),
130
+ )
131
+
132
+
133
+ def resolve_model(
134
+ endpoints: Iterable[ServingEndpointSummary | dict[str, object]],
135
+ *,
136
+ explicit: str | None = None,
137
+ fuzzy: bool = True,
138
+ threshold: float = DEFAULT_FUZZY_THRESHOLD,
139
+ requires_tools: bool = False,
140
+ model_class: ModelClass | None = None,
141
+ fallbacks: Sequence[str] = (),
142
+ ) -> ResolvedModelSelection:
143
+ summaries = [_summary(endpoint) for endpoint in endpoints]
144
+ if explicit is not None:
145
+ if not fuzzy:
146
+ if requires_tools:
147
+ _assert_tool_support(summaries, explicit)
148
+ return ResolvedModelSelection(modelId=explicit, source="explicit")
149
+ ranked = rank_models(
150
+ summaries,
151
+ ModelQuery(
152
+ search=explicit,
153
+ modelClass=model_class,
154
+ requiresTools=requires_tools,
155
+ threshold=threshold,
156
+ limit=1,
157
+ ),
158
+ )
159
+ if requires_tools and not ranked:
160
+ raise ValueError(f'No tool-capable model matches "{explicit}"')
161
+ model_id = str(ranked[0]["endpoint"]["name"]) if ranked else explicit
162
+ return ResolvedModelSelection(modelId=model_id, source="fuzzy-match")
163
+
164
+ if model_class is None and fallbacks:
165
+ present = {
166
+ endpoint.name
167
+ for endpoint in summaries
168
+ if not requires_tools or endpoint_capabilities(endpoint).tools
169
+ }
170
+ for fallback in fallbacks:
171
+ if fallback in present:
172
+ return ResolvedModelSelection(modelId=fallback, source="fallback")
173
+
174
+ source = "class" if model_class is not None else "fallback"
175
+ ranked = rank_models(
176
+ summaries,
177
+ ModelQuery(modelClass=model_class, requiresTools=requires_tools, limit=1),
178
+ )
179
+ if ranked:
180
+ return ResolvedModelSelection(modelId=str(ranked[0]["endpoint"]["name"]), source=source)
181
+
182
+ floor_source = models_for_class(model_class) if model_class is not None else list(fallbacks)
183
+ floor = list(dict.fromkeys([*floor_source, *FALLBACK_MODEL_IDS]))
184
+ if requires_tools:
185
+ available = {
186
+ endpoint.name for endpoint in summaries if endpoint_capabilities(endpoint).tools
187
+ }
188
+ selected = next((model_id for model_id in floor if model_id in available), None)
189
+ if selected is None:
190
+ raise ValueError("No tool-capable model is available")
191
+ return ResolvedModelSelection(modelId=selected, source=source)
192
+ present = {endpoint.name for endpoint in summaries}
193
+ selected = next((model_id for model_id in floor if model_id in present), floor[0])
194
+ return ResolvedModelSelection(modelId=selected, source=source)
195
+
196
+
197
+ def _token_distance(token: str, name: str) -> float:
198
+ if token in name:
199
+ return 0.0
200
+ segments = re.findall(r"[a-z0-9]+", name)
201
+ similarity = max(
202
+ (SequenceMatcher(None, token, segment).ratio() for segment in segments), default=0
203
+ )
204
+ return 1 - similarity
205
+
206
+
207
+ def _assert_tool_support(endpoints: list[ServingEndpointSummary], model_id: str) -> None:
208
+ endpoint = next((candidate for candidate in endpoints if candidate.name == model_id), None)
209
+ if endpoint is None or not endpoint_capabilities(endpoint).tools:
210
+ raise ValueError(f'Model "{model_id}" does not support function tools')
211
+
212
+
213
+ def _summary(value: ServingEndpointSummary | dict[str, object]) -> ServingEndpointSummary:
214
+ return (
215
+ value
216
+ if isinstance(value, ServingEndpointSummary)
217
+ else ServingEndpointSummary.model_validate(value)
218
+ )
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import re
5
+ from collections.abc import Iterable, Mapping
6
+ from typing import Any, Protocol
7
+
8
+ from .classes import MODEL_CLASS_ORDER
9
+ from .classify import classified_summaries, supports_tools_by_family
10
+ from .models import ModelProfile, ServingEndpointSummary
11
+
12
+
13
+ class ServingEndpointsApi(Protocol):
14
+ def list(self) -> Iterable[object]: ...
15
+
16
+
17
+ class WorkspaceClientLike(Protocol):
18
+ serving_endpoints: ServingEndpointsApi
19
+
20
+
21
+ def list_serving_endpoints(client: WorkspaceClientLike) -> list[ServingEndpointSummary]:
22
+ summaries = list_serving_endpoints_uncached(client)
23
+ buckets = classified_summaries(summaries)
24
+ classes = {
25
+ endpoint.name: model_class
26
+ for model_class in MODEL_CLASS_ORDER
27
+ for endpoint in buckets[model_class]
28
+ }
29
+ for summary in summaries:
30
+ summary.model_class = classes.get(summary.name)
31
+ return summaries
32
+
33
+
34
+ def list_serving_endpoints_uncached(client: WorkspaceClientLike) -> list[ServingEndpointSummary]:
35
+ summaries = []
36
+ for endpoint in client.serving_endpoints.list():
37
+ name = _value(endpoint, "name")
38
+ if not isinstance(name, str) or not name:
39
+ continue
40
+ summaries.append(
41
+ ServingEndpointSummary(
42
+ name=name,
43
+ displayName=to_model_display_name(name, _provided_display_name(endpoint)),
44
+ task=_string(_value(endpoint, "task")),
45
+ state=_string(_value(_value(endpoint, "state"), "ready")),
46
+ description=_string(_value(endpoint, "description")),
47
+ supportsTools=supports_tools_by_family(name),
48
+ profile=_extract_profile(endpoint),
49
+ )
50
+ )
51
+ return summaries
52
+
53
+
54
+ def to_model_display_name(name: str, provided: str | None = None) -> str:
55
+ if provided is not None and provided.strip():
56
+ return provided.strip()
57
+ segments = [segment for segment in re.split(r"[-_.\s/]+", name) if segment]
58
+ start = 0
59
+ while start < len(segments) and segments[start].lower() in {"databricks", "system", "dbx"}:
60
+ if (
61
+ segments[start].lower() == "system"
62
+ and start + 1 < len(segments)
63
+ and segments[start + 1].lower() == "ai"
64
+ ):
65
+ start += 1
66
+ start += 1
67
+ source = segments[start:] or segments
68
+ acronyms = {"gpt", "gte", "bge", "dbrx", "oss", "llm", "moe", "ai"}
69
+ pieces = []
70
+ numeric = []
71
+ for segment in source:
72
+ if segment.isdigit():
73
+ numeric.append(segment)
74
+ continue
75
+ if numeric:
76
+ pieces.append(".".join(numeric))
77
+ numeric = []
78
+ size = re.fullmatch(r"(\d+)([bmk])", segment, flags=re.IGNORECASE)
79
+ if size:
80
+ pieces.append(f"{size.group(1)}{size.group(2).upper()}")
81
+ else:
82
+ lower = segment.lower()
83
+ pieces.append(lower.upper() if lower in acronyms else lower.capitalize())
84
+ if numeric:
85
+ pieces.append(".".join(numeric))
86
+ return " ".join(pieces) or name.strip()
87
+
88
+
89
+ def _extract_profile(endpoint: object) -> ModelProfile | None:
90
+ config = _value(endpoint, "config")
91
+ entities = _value(config, "served_entities") or []
92
+ for entity in entities:
93
+ foundation = _value(entity, "foundation_model")
94
+ raw = _value(foundation, "ai_gateway_model_profile")
95
+ values = {
96
+ key: value
97
+ for key in ("quality", "speed", "cost")
98
+ if isinstance((value := _value(raw, key)), (int, float)) and math.isfinite(value)
99
+ }
100
+ if values:
101
+ return ModelProfile.model_validate(values)
102
+ return None
103
+
104
+
105
+ def _provided_display_name(endpoint: object) -> str | None:
106
+ for tag in _value(endpoint, "tags") or []:
107
+ if _value(tag, "key") in {"display_name", "displayName", "name"}:
108
+ value = _string(_value(tag, "value"))
109
+ if value and value.strip():
110
+ return value.strip()
111
+ config = _value(endpoint, "config")
112
+ for entity in _value(config, "served_entities") or []:
113
+ value = _string(_value(_value(entity, "external_model"), "name"))
114
+ if value and value.strip():
115
+ return value.strip()
116
+ return None
117
+
118
+
119
+ def _value(value: object, key: str) -> Any:
120
+ if isinstance(value, Mapping):
121
+ return value.get(key)
122
+ return getattr(value, key, None)
123
+
124
+
125
+ def _string(value: object) -> str | None:
126
+ return None if value is None else str(value)