mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_/key-info-badge-styling-b293cc
This commit is contained in:
commit
f9f016dc1d
9 changed files with 182 additions and 17 deletions
|
|
@ -1599,6 +1599,9 @@ class MCPServerManager:
|
|||
manual_token_url,
|
||||
)
|
||||
use_issuer_anchor = _uses_issuer_anchor(manual_issuer, is_discovery_auth_type or obo_needs_discovery)
|
||||
configured_authorization_url = manual_authorization_url
|
||||
configured_token_url = manual_token_url
|
||||
configured_registration_url = manual_registration_url
|
||||
manual_authorization_url, manual_token_url, manual_registration_url = _endpoints_yield_to_issuer(
|
||||
manual_issuer,
|
||||
is_discovery_auth_type,
|
||||
|
|
@ -1725,6 +1728,9 @@ class MCPServerManager:
|
|||
authorization_url=resolved_authorization_url,
|
||||
token_url=resolved_token_url,
|
||||
registration_url=resolved_registration_url,
|
||||
configured_authorization_url=configured_authorization_url,
|
||||
configured_token_url=configured_token_url,
|
||||
configured_registration_url=configured_registration_url,
|
||||
token_endpoint_auth_method=server_config.get("token_endpoint_auth_method", None),
|
||||
# TODO: utility fn the default values
|
||||
transport=server_config.get("transport", MCPTransport.http),
|
||||
|
|
@ -2170,6 +2176,9 @@ class MCPServerManager:
|
|||
is_discovery_auth_type
|
||||
or self._obo_needs_endpoint_discovery(auth_type, token_exchange_endpoint, manual_token_url),
|
||||
)
|
||||
configured_authorization_url: Final = manual_authorization_url
|
||||
configured_token_url: Final = manual_token_url
|
||||
configured_registration_url: Final = manual_registration_url
|
||||
manual_authorization_url, manual_token_url, manual_registration_url = _endpoints_yield_to_issuer(
|
||||
manual_issuer,
|
||||
is_discovery_auth_type,
|
||||
|
|
@ -2222,6 +2231,9 @@ class MCPServerManager:
|
|||
authorization_url=manual_authorization_url or getattr(gated_oauth_metadata, "authorization_url", None),
|
||||
token_url=manual_token_url or getattr(gated_oauth_metadata, "token_url", None),
|
||||
registration_url=manual_registration_url or getattr(gated_oauth_metadata, "registration_url", None),
|
||||
configured_authorization_url=configured_authorization_url,
|
||||
configured_token_url=configured_token_url,
|
||||
configured_registration_url=configured_registration_url,
|
||||
token_endpoint_auth_method=(
|
||||
credentials_dict.get("token_endpoint_auth_method") if credentials_dict else None
|
||||
),
|
||||
|
|
@ -5858,9 +5870,9 @@ class MCPServerManager:
|
|||
args=getattr(server, "args", None) or [],
|
||||
env=getattr(server, "env", None) or {},
|
||||
issuer=server.issuer,
|
||||
authorization_url=server.authorization_url,
|
||||
token_url=server.token_url,
|
||||
registration_url=server.registration_url,
|
||||
authorization_url=server.configured_authorization_url or server.authorization_url,
|
||||
token_url=server.configured_token_url or server.token_url,
|
||||
registration_url=server.configured_registration_url or server.registration_url,
|
||||
oauth2_flow=server.oauth2_flow,
|
||||
dcr_bridge=server.dcr_bridge,
|
||||
token_exchange_endpoint=server.token_exchange_endpoint,
|
||||
|
|
@ -5968,9 +5980,9 @@ class MCPServerManager:
|
|||
args=getattr(server, "args", None) or [],
|
||||
env=getattr(server, "env", None) or {},
|
||||
issuer=server.issuer,
|
||||
authorization_url=server.authorization_url,
|
||||
token_url=server.token_url,
|
||||
registration_url=server.registration_url,
|
||||
authorization_url=server.configured_authorization_url or server.authorization_url,
|
||||
token_url=server.configured_token_url or server.token_url,
|
||||
registration_url=server.configured_registration_url or server.registration_url,
|
||||
oauth2_flow=server.oauth2_flow,
|
||||
token_exchange_endpoint=server.token_exchange_endpoint,
|
||||
audience=server.audience,
|
||||
|
|
|
|||
|
|
@ -2980,7 +2980,7 @@
|
|||
},
|
||||
{
|
||||
"provider": "Hosted_Vllm",
|
||||
"provider_display_name": "vllm",
|
||||
"provider_display_name": "Hosted vLLM",
|
||||
"litellm_provider": "hosted_vllm",
|
||||
"credential_fields": [
|
||||
{
|
||||
|
|
@ -3008,7 +3008,7 @@
|
|||
},
|
||||
{
|
||||
"provider": "VLLM",
|
||||
"provider_display_name": "Vllm",
|
||||
"provider_display_name": "Local vLLM",
|
||||
"litellm_provider": "vllm",
|
||||
"credential_fields": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -71,6 +71,12 @@ class MCPServer(BaseModel):
|
|||
authorization_url: str | None = None
|
||||
token_url: str | None = None
|
||||
registration_url: str | None = None
|
||||
# Endpoints exactly as an admin stored them, unlike the resolved fields above which an anchored
|
||||
# issuer empties (RFC 8414 section 3.3). Management reads serve these so the edit form does not
|
||||
# load blanks and then save those blanks over the stored config.
|
||||
configured_authorization_url: str | None = None
|
||||
configured_token_url: str | None = None
|
||||
configured_registration_url: str | None = None
|
||||
# How the gateway authenticates to the upstream token endpoint. When
|
||||
# "client_secret_basic" the credentials go in an HTTP Basic Authorization
|
||||
# header (omitted from the body); None defaults to "client_secret_post".
|
||||
|
|
|
|||
|
|
@ -479,6 +479,35 @@ class TestMCPServerManager:
|
|||
assert server.oauth2_flow == "authorization_code"
|
||||
assert server.needs_user_oauth_token is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_servers_from_config_keeps_configured_endpoints_for_management_view(self):
|
||||
"""A yaml server with a pinned issuer still reports its configured endpoints to the management
|
||||
view, even though the runtime fields are empty because the anchored issuer is the sole endpoint
|
||||
source. The dashboard edits that view, so emptied values there load as blank fields and the next
|
||||
save writes the blanks over the config."""
|
||||
manager = MCPServerManager()
|
||||
|
||||
config = self._oauth2_config(
|
||||
oauth2_flow="authorization_code",
|
||||
issuer="https://idp.example.com",
|
||||
authorization_url="https://example.com/oauth/authorize",
|
||||
token_url="https://example.com/oauth/token",
|
||||
registration_url="https://example.com/oauth/register",
|
||||
)
|
||||
with patch.object(manager, "_fetch_issuer_anchored_oauth_metadata", new=AsyncMock(return_value=None)):
|
||||
await manager.load_servers_from_config(config)
|
||||
|
||||
server = next(iter(manager.config_mcp_servers.values()))
|
||||
assert server.authorization_url is None
|
||||
assert server.token_url is None
|
||||
assert server.registration_url is None
|
||||
|
||||
view = manager._build_mcp_server_table(server)
|
||||
|
||||
assert view.authorization_url == "https://example.com/oauth/authorize"
|
||||
assert view.token_url == "https://example.com/oauth/token"
|
||||
assert view.registration_url == "https://example.com/oauth/register"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_servers_from_config_rejects_uncorroborated_endpoints_but_keeps_resource_scopes(self):
|
||||
"""A yaml server with a manual authorization_url has the same config-time mix-up exposure as a
|
||||
|
|
@ -1611,6 +1640,43 @@ class TestMCPServerManager:
|
|||
assert built.token_url == "https://idp.example.com/token"
|
||||
assert built.token_url != "https://attacker.example.com/steal"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_management_view_keeps_stored_endpoints_when_issuer_is_pinned(self):
|
||||
"""A pinned issuer empties the endpoints the runtime uses, but the management view must still
|
||||
report what the admin stored. Serving the emptied values made the dashboard edit form load the
|
||||
three endpoint fields blank, so saving with no edits sent them back as explicit nulls and wiped
|
||||
the row, and re-entering them looked like it never saved."""
|
||||
manager = MCPServerManager()
|
||||
row = LiteLLM_MCPServerTable(
|
||||
server_id="issuer-anchored-management-view",
|
||||
alias="issuer_anchored_management_view",
|
||||
description="issuer pinned with admin-entered endpoints",
|
||||
url="https://up.example.com/mcp",
|
||||
transport=MCPTransport.http,
|
||||
auth_type=MCPAuth.oauth2,
|
||||
oauth2_flow="authorization_code",
|
||||
issuer="https://idp.example.com",
|
||||
authorization_url="https://up.example.com/oauth/authorize",
|
||||
token_url="https://up.example.com/oauth/token",
|
||||
registration_url="https://up.example.com/oauth/register",
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
|
||||
with patch.object(manager, "_fetch_issuer_anchored_oauth_metadata", new=AsyncMock(return_value=None)):
|
||||
built = await manager.build_mcp_server_from_table(row, credentials_are_encrypted=False)
|
||||
|
||||
assert built.authorization_url is None
|
||||
assert built.token_url is None
|
||||
assert built.registration_url is None
|
||||
|
||||
view = manager._build_mcp_server_table(built)
|
||||
|
||||
assert view.issuer == "https://idp.example.com"
|
||||
assert view.authorization_url == "https://up.example.com/oauth/authorize"
|
||||
assert view.token_url == "https://up.example.com/oauth/token"
|
||||
assert view.registration_url == "https://up.example.com/oauth/register"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"advertised_authorization_url",
|
||||
|
|
|
|||
|
|
@ -243,6 +243,33 @@ def test_bedrock_mantle_provider_fields():
|
|||
assert fields_by_key["api_base"]["field_type"] == "text"
|
||||
|
||||
|
||||
def test_vllm_provider_display_names_are_distinct():
|
||||
"""Hosted and local vLLM must not share a dropdown label.
|
||||
|
||||
The Add Model provider dropdown is driven by /public/providers/fields.
|
||||
Both entries previously rendered as near-identical "vllm"/"Vllm" rows
|
||||
with the same logo, so admins could not tell them apart.
|
||||
"""
|
||||
app_instance = FastAPI()
|
||||
app_instance.include_router(router)
|
||||
test_client = TestClient(app_instance)
|
||||
|
||||
response = test_client.get("/public/providers/fields")
|
||||
assert response.status_code == 200
|
||||
providers = response.json()
|
||||
|
||||
hosted = next((p for p in providers if p["provider"] == "Hosted_Vllm"), None)
|
||||
local = next((p for p in providers if p["provider"] == "VLLM"), None)
|
||||
assert hosted is not None, "Hosted vLLM provider entry not found"
|
||||
assert local is not None, "Local vLLM provider entry not found"
|
||||
|
||||
assert hosted["provider_display_name"] == "Hosted vLLM"
|
||||
assert local["provider_display_name"] == "Local vLLM"
|
||||
assert hosted["provider_display_name"].casefold() != local["provider_display_name"].casefold()
|
||||
assert hosted["litellm_provider"] == "hosted_vllm"
|
||||
assert local["litellm_provider"] == "vllm"
|
||||
|
||||
|
||||
def test_nvidia_riva_provider_fields():
|
||||
app_instance = FastAPI()
|
||||
app_instance.include_router(router)
|
||||
|
|
|
|||
|
|
@ -66,6 +66,36 @@ const totals = (overrides: Partial<Totals> = {}): Totals => ({
|
|||
...overrides,
|
||||
});
|
||||
|
||||
const zeroBucket = { turns: 0, hits: 0, hit_rate_pct: 0 };
|
||||
|
||||
const zeroCache: AutoRouterCacheStats = {
|
||||
coverage_pct: 0,
|
||||
hit_rate_pct: 0,
|
||||
same_model: zeroBucket,
|
||||
first_visit: zeroBucket,
|
||||
return_to_tier: zeroBucket,
|
||||
unordered_turns: 0,
|
||||
return_misses_expired: 0,
|
||||
return_misses_within_ttl: 0,
|
||||
return_misses_unknown: 0,
|
||||
ttl_5m_turns: 0,
|
||||
ttl_1h_turns: 0,
|
||||
};
|
||||
|
||||
const zeroTotals: Totals = {
|
||||
sessions: 0,
|
||||
turns: 0,
|
||||
avg_turns_per_session: 0,
|
||||
avg_session_seconds: 0,
|
||||
avg_tokens_per_session: 0,
|
||||
spend: 0,
|
||||
saved_spend: 0,
|
||||
baseline_spend: 0,
|
||||
saved_pct: 0,
|
||||
saved_per_session: 0,
|
||||
cache: zeroCache,
|
||||
};
|
||||
|
||||
const group = (overrides: Partial<AutoRouterBenchmarkGroup> = {}): AutoRouterBenchmarkGroup => ({
|
||||
router_name: "claude-auto",
|
||||
router_type: "complexity",
|
||||
|
|
@ -162,6 +192,7 @@ describe("AutoRouterBenchmarksTab", () => {
|
|||
expect(screen.getByText("97.7%")).toBeInTheDocument();
|
||||
expect(screen.getByText("24.3%")).toBeInTheDocument();
|
||||
expect(screen.getByText("81.6%")).toBeInTheDocument();
|
||||
expect(screen.getByRole("img", { name: "Share of turns by bucket" })).not.toHaveClass("bg-muted");
|
||||
});
|
||||
|
||||
it("summarizes the cache column from the bucketed turns, not the session turns", () => {
|
||||
|
|
@ -252,11 +283,25 @@ describe("AutoRouterBenchmarksTab", () => {
|
|||
expect(screen.getByText("Auto-router usage is unavailable right now")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("says so when there are no auto-router sessions at all", () => {
|
||||
mockHook({ data: response([]) });
|
||||
it("renders the full dashboard with zeroed stats when the window has no sessions", () => {
|
||||
mockHook({ data: response([], zeroTotals) });
|
||||
renderTab();
|
||||
|
||||
expect(screen.getByText("No auto-router sessions in this window yet")).toBeInTheDocument();
|
||||
expect(screen.getByText("Total estimated savings")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("$0.00")).toHaveLength(4);
|
||||
expect(screen.getByText("across 0 sessions")).toBeInTheDocument();
|
||||
expect(screen.getByText("0s")).toBeInTheDocument();
|
||||
expect(screen.getByText(/turns measured/)).toBeInTheDocument();
|
||||
expect(screen.getAllByText("0.0%").length).toBeGreaterThan(0);
|
||||
expect(screen.getByRole("img", { name: "Share of turns by bucket" })).toHaveClass("bg-muted");
|
||||
});
|
||||
|
||||
it("shows the savings delta as an unsigned zero when nothing was saved", () => {
|
||||
mockHook({ data: response([], zeroTotals) });
|
||||
renderTab();
|
||||
|
||||
expect(screen.getByText("0%")).toBeInTheDocument();
|
||||
expect(screen.queryByText("-0%")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("requests the default thirty day window and widens or narrows it from the picker", () => {
|
||||
|
|
@ -303,7 +348,7 @@ describe("AutoRouterBenchmarksTab", () => {
|
|||
});
|
||||
|
||||
it("keeps the window picker reachable while a window has no sessions", () => {
|
||||
mockHook({ data: response([]) });
|
||||
mockHook({ data: response([], zeroTotals) });
|
||||
renderTab();
|
||||
|
||||
expect(screen.getByRole("tab", { name: "30d" })).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ const HeroCard: React.FC<{ view: BenchmarkView }> = ({ view }) => {
|
|||
variant="secondary"
|
||||
className={cheaper ? "bg-emerald-50 text-emerald-700" : "bg-red-50 text-destructive"}
|
||||
>
|
||||
{cheaper ? "-" : "+"}
|
||||
{stats.saved_spend !== 0 && (cheaper ? "-" : "+")}
|
||||
{Math.abs(stats.saved_pct).toFixed(0)}%
|
||||
</Badge>
|
||||
</div>
|
||||
|
|
@ -95,7 +95,7 @@ const StackedTurnBar: React.FC<{ buckets: BucketRow[] }> = ({ buckets }) => {
|
|||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div
|
||||
className="flex h-2.5 w-full gap-0.5 overflow-hidden rounded-sm"
|
||||
className={`flex h-2.5 w-full gap-0.5 overflow-hidden rounded-sm ${segments.length === 0 ? "bg-muted" : ""}`}
|
||||
role="img"
|
||||
aria-label="Share of turns by bucket"
|
||||
>
|
||||
|
|
@ -230,7 +230,6 @@ const BenchmarksBody: React.FC<BenchmarksBodyProps> = ({ isPending, error, data,
|
|||
return <Message>Auto-router usage is visible to proxy admin roles only</Message>;
|
||||
}
|
||||
if (error || !data) return <Message>Auto-router usage is unavailable right now</Message>;
|
||||
if (data.groups.length === 0) return <Message>No auto-router sessions in this window yet</Message>;
|
||||
|
||||
const view = viewFor(data, selectedKey);
|
||||
const stats = view.stats;
|
||||
|
|
|
|||
|
|
@ -94,6 +94,16 @@ describe("provider_info_helpers", () => {
|
|||
expect(result.displayName).toBe(Providers.ZAI);
|
||||
});
|
||||
|
||||
it("should give hosted_vllm and vllm distinct display names", () => {
|
||||
const hosted = getProviderLogoAndName("hosted_vllm");
|
||||
const local = getProviderLogoAndName("vllm");
|
||||
expect(hosted.displayName).toBe("Hosted vLLM");
|
||||
expect(local.displayName).toBe("Local vLLM");
|
||||
expect(hosted.displayName.toLowerCase()).not.toBe(local.displayName.toLowerCase());
|
||||
expect(hosted.logo).toBe(providerLogoMap[Providers.Hosted_Vllm]);
|
||||
expect(local.logo).toBe(providerLogoMap[Providers.VLLM]);
|
||||
});
|
||||
|
||||
it("should resolve the nvidia_riva provider value to the Nvidia Riva display name and logo", () => {
|
||||
const result = getProviderLogoAndName("nvidia_riva");
|
||||
expect(result.displayName).toBe(Providers.NVIDIA_RIVA);
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ export enum Providers {
|
|||
GradientAI = "GradientAI",
|
||||
Groq = "Groq",
|
||||
HEROKU = "Heroku",
|
||||
Hosted_Vllm = "vllm",
|
||||
Hosted_Vllm = "Hosted vLLM",
|
||||
HUGGINGFACE = "Huggingface",
|
||||
HYPERBOLIC = "Hyperbolic",
|
||||
Infinity = "Infinity",
|
||||
|
|
@ -162,7 +162,7 @@ export enum Providers {
|
|||
VERCEL_AI_GATEWAY = "Vercel Ai Gateway",
|
||||
Vertex_AI = "Vertex AI (Anthropic, Gemini, etc.)",
|
||||
VERTEX_AI_BETA = "Vertex Ai Beta",
|
||||
VLLM = "Vllm",
|
||||
VLLM = "Local vLLM",
|
||||
VolcEngine = "VolcEngine",
|
||||
Voyage = "Voyage AI",
|
||||
WANDB = "Wandb",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue