fix(agents): keep the published agent in public_agent_groups

`POST /v1/agents/{id}/make_public` appended the agent id to
`litellm.public_agent_groups` and only then called `get_config()`, which
re-applies the DB's `litellm_settings` over the module globals and threw the
append away. The config it saved was therefore a no-op: the endpoint answered
200 with an empty `public_agent_groups`, the agent never reached
`GET /public/agent_hub`, and re-publishing never hit the "already public" 400.
Read the config first, derive the new list from the refreshed globals, save it,
then update the global

Also fixes the e2e model hub spec, which is flaky for a second reason: the
"Make Models Public" modal preselects the groups that are already public, so a
blind click on "Select All" cleared them and left "Next" disabled for the full
15s action timeout. Check the box instead of toggling it, and wait for "Next"
to be enabled before clicking
This commit is contained in:
mateo-berri 2026-09-03 04:24:09 -07:00
parent 658f50663d
commit ccbd3e495c
3 changed files with 87 additions and 16 deletions

View file

@ -939,35 +939,34 @@ async def make_agent_public(
if agent is None:
raise HTTPException(status_code=404, detail=f"Agent with ID {agent_id} not found")
if litellm.public_agent_groups is None:
litellm.public_agent_groups = []
# handle duplicates
if not AGENT_REGISTRY.ids_for_agent(agent.agent_id).isdisjoint(litellm.public_agent_groups):
# get_config() re-applies the DB's litellm_settings over the in-memory
# globals, so read it before deriving the new list and assign the global after
config: Final = await proxy_config.get_config()
current_public_agent_groups: Final = list(litellm.public_agent_groups or [])
if not AGENT_REGISTRY.ids_for_agent(agent.agent_id).isdisjoint(current_public_agent_groups):
raise HTTPException(
status_code=400,
detail=f"Agent with name {agent.agent_name} already in public agent groups",
)
litellm.public_agent_groups.append(agent.agent_id)
updated_public_agent_groups: Final = [*current_public_agent_groups, agent.agent_id]
# Load existing config
config: Final = await proxy_config.get_config()
# Update config with new settings
if "litellm_settings" not in config or config["litellm_settings"] is None:
config["litellm_settings"] = {}
config["litellm_settings"]["public_agent_groups"] = litellm.public_agent_groups
config["litellm_settings"]["public_agent_groups"] = updated_public_agent_groups
# Save the updated config
await proxy_config.save_config(new_config=config)
litellm.public_agent_groups = updated_public_agent_groups
verbose_proxy_logger.debug(
"Updated public agent groups to: %s by user: %s", litellm.public_agent_groups, user_api_key_dict.user_id
"Updated public agent groups to: %s by user: %s", updated_public_agent_groups, user_api_key_dict.user_id
)
return {
"message": "Successfully updated public agent groups",
"public_agent_groups": litellm.public_agent_groups,
"public_agent_groups": updated_public_agent_groups,
"updated_by": user_api_key_dict.user_id,
}
except HTTPException:

View file

@ -22,11 +22,14 @@ test.describe("AI Hub (internal admin view)", () => {
// on the disabled-Next button or the success toast.
await expect(modal.getByText(/Select All \(\d+\)/)).toBeVisible({ timeout: 5_000 });
// Step 1: pick the seeded models via "Select All"
await modal.getByText(/Select All/i).click();
// Step 1: pick the seeded models via "Select All". check() rather than click() because
// the modal preselects groups that are already public, and a click would clear them.
await modal.getByRole("checkbox", { name: /Select All/ }).check();
// Move to confirm step
await modal.getByRole("button", { name: "Next" }).click();
const next = modal.getByRole("button", { name: "Next" });
await expect(next).toBeEnabled();
await next.click();
await expect(modal.getByText("Confirm Making Models Public")).toBeVisible({ timeout: 5_000 });
// Submit

View file

@ -1062,3 +1062,72 @@ def test_merged_agent_card_url_has_no_double_slash_without_proxy_base_url(
interface_url = merged["supportedInterfaces"][0]["url"]
assert interface_url == f"{base_url.rstrip('/')}/a2a/agent-xyz"
assert "//a2a" not in interface_url
class _DbBackedProxyConfig:
"""Round-trips `litellm_settings` through the DB overlay the proxy applies on every
`get_config()`, which is what re-assigns the `litellm.public_*` globals in production."""
def __init__(self) -> None:
self.stored_litellm_settings: dict = {}
async def get_config(self) -> dict:
from litellm.proxy.proxy_server import ProxyConfig
config: dict = {"litellm_settings": {}}
if not self.stored_litellm_settings:
return config
return ProxyConfig()._update_config_fields(
current_config=config,
param_name="litellm_settings",
db_param_value=dict(self.stored_litellm_settings),
)
async def save_config(self, new_config: dict) -> None:
self.stored_litellm_settings = dict(new_config.get("litellm_settings") or {})
def test_make_agent_public_twice_keeps_both_agents_public(monkeypatch):
"""A second /make_public call must not drop the agent published by the first one."""
import litellm
from litellm.proxy.agent_endpoints import agent_registry as agent_registry_module
from litellm.proxy.agent_endpoints.agent_registry import AgentRegistry
registry = AgentRegistry()
registry.register_agent(_sample_agent_response(agent_id="agent-1", agent_name="Agent One"))
registry.register_agent(_sample_agent_response(agent_id="agent-2", agent_name="Agent Two"))
monkeypatch.setattr(agent_registry_module, "global_agent_registry", registry)
monkeypatch.setattr(litellm, "public_agent_groups", None)
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", MagicMock())
monkeypatch.setattr("litellm.proxy.proxy_server.proxy_config", _DbBackedProxyConfig())
first = client.post("/v1/agents/agent-1/make_public", headers={"Authorization": "Bearer test-key"})
second = client.post("/v1/agents/agent-2/make_public", headers={"Authorization": "Bearer test-key"})
assert first.status_code == 200
assert second.status_code == 200
assert second.json()["public_agent_groups"] == ["agent-1", "agent-2"]
assert [agent.agent_id for agent in registry.get_public_agent_list()] == ["agent-1", "agent-2"]
def test_make_agent_public_rejects_an_already_public_agent(monkeypatch):
"""The duplicate guard must still fire when the published list comes back from the DB."""
import litellm
from litellm.proxy.agent_endpoints import agent_registry as agent_registry_module
from litellm.proxy.agent_endpoints.agent_registry import AgentRegistry
registry = AgentRegistry()
registry.register_agent(_sample_agent_response(agent_id="agent-1", agent_name="Agent One"))
monkeypatch.setattr(agent_registry_module, "global_agent_registry", registry)
monkeypatch.setattr(litellm, "public_agent_groups", None)
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", MagicMock())
monkeypatch.setattr("litellm.proxy.proxy_server.proxy_config", _DbBackedProxyConfig())
first = client.post("/v1/agents/agent-1/make_public", headers={"Authorization": "Bearer test-key"})
duplicate = client.post("/v1/agents/agent-1/make_public", headers={"Authorization": "Bearer test-key"})
assert first.status_code == 200
assert duplicate.status_code == 400
assert "already in public agent groups" in duplicate.json()["detail"]