mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-25 01:02:15 +00:00
Merge pull request #42121 from BerriAI/litellm_utils_model_info_lookup
feat(proxy): add GET /utils/model_info to look up cost map info for unregistered models
This commit is contained in:
commit
cc1a3157d3
6 changed files with 168 additions and 0 deletions
|
|
@ -403,6 +403,7 @@ class LiteLLMRoutes(enum.Enum):
|
|||
"/v1/models",
|
||||
# token counter
|
||||
"/utils/token_counter",
|
||||
"/utils/model_info",
|
||||
"/utils/transform_request",
|
||||
# rerank
|
||||
"/rerank",
|
||||
|
|
|
|||
|
|
@ -845,6 +845,7 @@ MODEL_DISCOVERY_ROUTES: Final = frozenset(
|
|||
"/v1/model/info",
|
||||
"/v2/model/info",
|
||||
"/model_group/info",
|
||||
"/utils/model_info",
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -13476,6 +13476,48 @@ async def supported_openai_params(model: str):
|
|||
raise HTTPException(status_code=400, detail={"error": f"Could not map model={model}"})
|
||||
|
||||
|
||||
class _ModelInfoLookupResponse(TypedDict):
|
||||
model: ReadOnly[str]
|
||||
custom_llm_provider: ReadOnly[str]
|
||||
model_info: ReadOnly[Mapping[str, object]]
|
||||
|
||||
|
||||
@router.get(
|
||||
"/utils/model_info",
|
||||
tags=["llm utils"], # mutable-ok: FastAPI tags kwarg is list-typed
|
||||
dependencies=[Depends(user_api_key_auth)], # mutable-ok: FastAPI dependencies kwarg is list-typed
|
||||
)
|
||||
async def model_info_lookup(model: str, custom_llm_provider: str | None = None):
|
||||
"""
|
||||
Returns the model cost map entry (token limits, pricing, supports_* capabilities) for any model
|
||||
in the cost map, whether or not it is registered on this proxy. `model_info` carries every
|
||||
field of the raw cost map entry plus the typed fields `litellm.get_model_info` derives from it
|
||||
(`key`, `supported_openai_params`).
|
||||
|
||||
Example curl:
|
||||
```
|
||||
curl -X GET --location 'http://localhost:4000/utils/model_info?model=gpt-4o&custom_llm_provider=openai' \
|
||||
--header 'Authorization: Bearer sk-1234'
|
||||
```
|
||||
"""
|
||||
detail: Final = { # mutable-ok: FastAPI serializes detail as a plain dict
|
||||
"error": f"model={model}, custom_llm_provider={custom_llm_provider} is not in the model cost map"
|
||||
}
|
||||
try:
|
||||
typed_model_info: Final = litellm.get_model_info(model=model, custom_llm_provider=custom_llm_provider)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=404, detail=detail)
|
||||
cost_map_entry: Final = litellm.model_cost.get(typed_model_info["key"])
|
||||
if cost_map_entry is None:
|
||||
raise HTTPException(status_code=404, detail=detail)
|
||||
response: Final[_ModelInfoLookupResponse] = {
|
||||
"model": model,
|
||||
"custom_llm_provider": typed_model_info["litellm_provider"],
|
||||
"model_info": {**typed_model_info, **cost_map_entry},
|
||||
}
|
||||
return response
|
||||
|
||||
|
||||
@router.post(
|
||||
"/utils/transform_request",
|
||||
tags=["llm utils"],
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ from litellm.constants import (
|
|||
REGISTRY_ERROR_NEGATIVE_CACHE_TTL,
|
||||
TAG_REGISTRY_MAX_SIZE,
|
||||
)
|
||||
from litellm.proxy.auth.route_checks import RouteChecks
|
||||
from litellm.proxy.common_utils.encrypt_decrypt_utils import decrypt_value_helper
|
||||
from litellm.proxy.common_utils.user_api_key_cache import (
|
||||
END_USER_RESTRICTED_REGISTRY_OVERFLOW_SENTINEL,
|
||||
|
|
@ -8889,6 +8890,8 @@ def test_jwt_team_role_reaches_the_gateway_token_endpoint_by_default():
|
|||
def test_route_skips_budget_checks_marks_only_spend_free_routes() -> None:
|
||||
assert route_skips_budget_checks(route="/v1/models") is True
|
||||
assert route_skips_budget_checks(route="/spend/logs") is True
|
||||
assert route_skips_budget_checks(route="/utils/model_info") is True
|
||||
assert RouteChecks.is_llm_api_route(route="/utils/model_info") is True
|
||||
assert route_skips_budget_checks(route="/health") is False
|
||||
assert route_skips_budget_checks(route="/v1/chat/completions") is False
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
Pins (PR2):
|
||||
- POST /utils/token_counter
|
||||
- GET /utils/supported_openai_params
|
||||
- GET /utils/model_info
|
||||
- POST /utils/transform_request
|
||||
"""
|
||||
|
||||
|
|
@ -231,6 +232,66 @@ def test_supported_openai_params_invalid_model(client, auth_as, monkeypatch):
|
|||
assert "Could not map model" in response.text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /utils/model_info
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def lookup_fixture_model(monkeypatch):
|
||||
entry = {
|
||||
"litellm_provider": "openai",
|
||||
"mode": "chat",
|
||||
"max_input_tokens": 1234,
|
||||
"max_output_tokens": 56,
|
||||
"input_cost_per_token": 1e-6,
|
||||
"output_cost_per_token": 2e-6,
|
||||
"supports_vision": True,
|
||||
"deprecation_date": "2099-01-01",
|
||||
"supports_lookup_fixture_edit": True,
|
||||
}
|
||||
monkeypatch.setattr(proxy_server, "llm_router", None)
|
||||
monkeypatch.setitem(litellm.model_cost, "lookup-fixture-model", entry)
|
||||
litellm.get_model_info.cache_clear()
|
||||
litellm.utils._cached_get_model_info_helper.cache_clear()
|
||||
yield entry
|
||||
litellm.get_model_info.cache_clear()
|
||||
litellm.utils._cached_get_model_info_helper.cache_clear()
|
||||
|
||||
|
||||
def test_model_info_lookup_returns_full_cost_map_entry_for_unregistered_model(client, auth_as, lookup_fixture_model):
|
||||
"""Every raw cost map field comes back, including ones outside ``ModelInfoBase`` that ``get_model_info`` drops."""
|
||||
with auth_as():
|
||||
response = client.get(
|
||||
"/utils/model_info", params={"model": "lookup-fixture-model", "custom_llm_provider": "openai"}
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
body = response.json()
|
||||
assert body["model"] == "lookup-fixture-model"
|
||||
assert body["custom_llm_provider"] == "openai"
|
||||
assert body["model_info"]["key"] == "lookup-fixture-model"
|
||||
assert isinstance(body["model_info"]["supported_openai_params"], list)
|
||||
assert {k: body["model_info"][k] for k in lookup_fixture_model} == lookup_fixture_model
|
||||
|
||||
|
||||
def test_model_info_lookup_unknown_model_returns_404(client, auth_as, monkeypatch):
|
||||
monkeypatch.setattr(proxy_server, "llm_router", None)
|
||||
with auth_as():
|
||||
response = client.get("/utils/model_info", params={"model": "no-such-model-lit-7476"})
|
||||
assert response.status_code == 404, response.text
|
||||
assert "is not in the model cost map" in response.text
|
||||
|
||||
|
||||
def test_model_info_lookup_returns_404_when_typed_info_has_no_cost_map_entry(client, auth_as, monkeypatch):
|
||||
"""``get_model_info`` synthesizes info for huggingface fallbacks absent from ``model_cost``;
|
||||
with no raw entry the route must 404 rather than answer 200 with typed fields only."""
|
||||
monkeypatch.setattr(proxy_server, "llm_router", None)
|
||||
with auth_as():
|
||||
response = client.get("/utils/model_info", params={"model": "huggingface/not-in-map-org/not-in-map-model"})
|
||||
assert response.status_code == 404, response.text
|
||||
assert "is not in the model cost map" in response.text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# POST /utils/transform_request
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
60
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
60
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -17448,6 +17448,34 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/utils/model_info": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/**
|
||||
* Model Info Lookup
|
||||
* @description Returns the model cost map entry (token limits, pricing, supports_* capabilities) for any model
|
||||
* in the cost map, whether or not it is registered on this proxy. `model_info` carries every
|
||||
* field of the raw cost map entry plus the typed fields `litellm.get_model_info` derives from it
|
||||
* (`key`, `supported_openai_params`).
|
||||
*
|
||||
* Example curl:
|
||||
* ```
|
||||
* curl -X GET --location 'http://localhost:4000/utils/model_info?model=gpt-4o&custom_llm_provider=openai' --header 'Authorization: Bearer sk-1234'
|
||||
* ```
|
||||
*/
|
||||
get: operations["model_info_lookup_utils_model_info_get"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/utils/supported_openai_params": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -63647,6 +63675,38 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
model_info_lookup_utils_model_info_get: {
|
||||
parameters: {
|
||||
query: {
|
||||
model: string;
|
||||
custom_llm_provider?: string | null;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": unknown;
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
supported_openai_params_utils_supported_openai_params_get: {
|
||||
parameters: {
|
||||
query: {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue