mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-17 04:34:25 +00:00
* test(integration): cover prompts_tags_caching endpoint contracts * test(integration): assert prompt lifecycle requests per environment * test(integration): pin non-latest prompt versions, label/path spellings and tag header/key spellings Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): cover production PATCH, streamed and root /responses prompts, and memory create scope Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): ignore proxy model discovery probes in prompt upstream doubles Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): prove tag update, peer prompt resync, memory team rows and Responses prompt spellings at runtime Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): cover memory metadata and key_prefix listing, multi-tag spellings for tag cache invalidation Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: mateo <mateo@berri.ai>
263 lines
10 KiB
Python
263 lines
10 KiB
Python
import hashlib
|
|
from typing import Final
|
|
|
|
import httpx
|
|
from integration._support.client import JSON_OBJECT, Gateway, object_value, string_value
|
|
from integration._support.database import read_rows, write_rows
|
|
from pydantic import BaseModel, JsonValue
|
|
|
|
_WORKFLOW_ROUTE: Final = ["/v1/workflows"]
|
|
|
|
|
|
class WorkflowRunResponse(BaseModel):
|
|
run_id: str
|
|
session_id: str
|
|
workflow_type: str
|
|
status: str
|
|
created_by: str | None = None
|
|
input: JsonValue | None = None
|
|
|
|
|
|
class WorkflowEventResponse(BaseModel):
|
|
event_id: str
|
|
run_id: str
|
|
event_type: str
|
|
step_name: str
|
|
sequence_number: int
|
|
|
|
|
|
class WorkflowMessageResponse(BaseModel):
|
|
message_id: str
|
|
run_id: str
|
|
role: str
|
|
content: str
|
|
sequence_number: int
|
|
|
|
|
|
def _delete_workflow_rows(run_id: str) -> None:
|
|
write_rows('DELETE FROM "LiteLLM_WorkflowEvent" WHERE run_id = %s', (run_id,))
|
|
write_rows('DELETE FROM "LiteLLM_WorkflowMessage" WHERE run_id = %s', (run_id,))
|
|
write_rows('DELETE FROM "LiteLLM_WorkflowRun" WHERE run_id = %s', (run_id,))
|
|
|
|
|
|
def _run_rows(run_id: str) -> list[dict[str, JsonValue]]:
|
|
return read_rows(
|
|
'SELECT run_id, workflow_type, status, created_by, input FROM "LiteLLM_WorkflowRun" WHERE run_id = %s',
|
|
(run_id,),
|
|
)
|
|
|
|
|
|
def _route_refusal(response: httpx.Response, user_id: str) -> None:
|
|
masked_user_id: Final = f"{user_id[:6]}{'*' * (len(user_id) - 8)}{user_id[-2:]}"
|
|
assert response.status_code == 401, response.text
|
|
assert JSON_OBJECT.validate_json(response.content) == {
|
|
"error": {
|
|
"message": (
|
|
"Authentication Error, Only proxy admin can be used to generate, delete, update info for new "
|
|
f"keys/users/teams. Route=/v1/workflows/runs. Your role=internal_user. Your user_id={masked_user_id}"
|
|
),
|
|
"type": "auth_error",
|
|
"param": "None",
|
|
"code": "401",
|
|
}
|
|
}, response.text
|
|
|
|
|
|
def test_workflow_runs_events_and_messages_are_key_owned(gateway: Gateway) -> None:
|
|
with gateway.scenario() as scenario:
|
|
default_user: Final = scenario.user(user_role="internal_user")
|
|
default_key: Final = scenario.key(user_id=default_user)
|
|
default_access: Final = gateway.request(
|
|
"POST",
|
|
"/v1/workflows/runs",
|
|
{"workflow_type": f"workflow-{default_user}", "input": {"source": "default"}},
|
|
key=default_key,
|
|
)
|
|
_route_refusal(default_access, default_user)
|
|
|
|
user_one: Final = scenario.user(user_role="internal_user")
|
|
user_two: Final = scenario.user(user_role="internal_user")
|
|
key_one: Final = scenario.key(user_id=user_one, allowed_routes=_WORKFLOW_ROUTE)
|
|
key_two: Final = scenario.key(user_id=user_two, allowed_routes=_WORKFLOW_ROUTE)
|
|
workflow_type: Final = f"workflow-{user_one}-{user_two}"
|
|
|
|
first_create: Final = gateway.request(
|
|
"POST",
|
|
"/v1/workflows/runs",
|
|
{"workflow_type": workflow_type, "input": {"owner": "one"}, "metadata": {"case": "ownership"}},
|
|
key=key_one,
|
|
)
|
|
second_create: Final = gateway.request(
|
|
"POST",
|
|
"/v1/workflows/runs",
|
|
{"workflow_type": workflow_type, "input": {"owner": "two"}},
|
|
key=key_two,
|
|
)
|
|
assert first_create.status_code == 200, first_create.text
|
|
assert second_create.status_code == 200, second_create.text
|
|
first_run: Final = WorkflowRunResponse.model_validate_json(first_create.content)
|
|
second_run: Final = WorkflowRunResponse.model_validate_json(second_create.content)
|
|
scenario.cleanups.callback(_delete_workflow_rows, first_run.run_id)
|
|
scenario.cleanups.callback(_delete_workflow_rows, second_run.run_id)
|
|
first_digest: Final = hashlib.sha256(key_one.encode()).hexdigest()
|
|
second_digest: Final = hashlib.sha256(key_two.encode()).hexdigest()
|
|
assert (first_run.status, first_run.created_by, first_run.input) == (
|
|
"pending",
|
|
first_digest,
|
|
{"owner": "one"},
|
|
), first_create.text
|
|
assert (second_run.status, second_run.created_by, second_run.input) == (
|
|
"pending",
|
|
second_digest,
|
|
{"owner": "two"},
|
|
), second_create.text
|
|
assert _run_rows(first_run.run_id) == [
|
|
{
|
|
"run_id": first_run.run_id,
|
|
"workflow_type": workflow_type,
|
|
"status": "pending",
|
|
"created_by": first_digest,
|
|
"input": {"owner": "one"},
|
|
}
|
|
]
|
|
assert _run_rows(second_run.run_id) == [
|
|
{
|
|
"run_id": second_run.run_id,
|
|
"workflow_type": workflow_type,
|
|
"status": "pending",
|
|
"created_by": second_digest,
|
|
"input": {"owner": "two"},
|
|
}
|
|
]
|
|
|
|
cross_key_operations: Final = (
|
|
gateway.request("GET", f"/v1/workflows/runs/{first_run.run_id}", key=key_two),
|
|
gateway.request(
|
|
"PATCH",
|
|
f"/v1/workflows/runs/{first_run.run_id}",
|
|
{"status": "completed"},
|
|
key=key_two,
|
|
),
|
|
gateway.request(
|
|
"POST",
|
|
f"/v1/workflows/runs/{first_run.run_id}/events",
|
|
{"event_type": "step.started", "step_name": "cross-key"},
|
|
key=key_two,
|
|
),
|
|
gateway.request(
|
|
"POST",
|
|
f"/v1/workflows/runs/{first_run.run_id}/messages",
|
|
{"role": "user", "content": "cross-key message"},
|
|
key=key_two,
|
|
),
|
|
gateway.request("GET", f"/v1/workflows/runs/{first_run.run_id}/events", key=key_two),
|
|
gateway.request("GET", f"/v1/workflows/runs/{first_run.run_id}/messages", key=key_two),
|
|
)
|
|
assert tuple((response.status_code, response.text) for response in cross_key_operations) == (
|
|
(404, f"""{{"detail":"Run '{first_run.run_id}' not found"}}"""),
|
|
) * len(cross_key_operations), tuple(response.text for response in cross_key_operations)
|
|
assert _run_rows(first_run.run_id)[0]["status"] == "pending"
|
|
assert (
|
|
read_rows(
|
|
'SELECT event_id FROM "LiteLLM_WorkflowEvent" WHERE run_id = %s',
|
|
(first_run.run_id,),
|
|
)
|
|
== []
|
|
)
|
|
assert (
|
|
read_rows(
|
|
'SELECT message_id FROM "LiteLLM_WorkflowMessage" WHERE run_id = %s',
|
|
(first_run.run_id,),
|
|
)
|
|
== []
|
|
)
|
|
|
|
first_list: Final = gateway.request(
|
|
"GET",
|
|
"/v1/workflows/runs",
|
|
params={"workflow_type": workflow_type},
|
|
key=key_one,
|
|
)
|
|
second_list: Final = gateway.request(
|
|
"GET",
|
|
"/v1/workflows/runs",
|
|
params={"workflow_type": workflow_type},
|
|
key=key_two,
|
|
)
|
|
assert first_list.status_code == 200, first_list.text
|
|
assert second_list.status_code == 200, second_list.text
|
|
assert tuple(string_value(object_value(run)["run_id"]) for run in object_value(first_list.json())["runs"]) == (
|
|
first_run.run_id,
|
|
), first_list.text
|
|
assert tuple(string_value(object_value(run)["run_id"]) for run in object_value(second_list.json())["runs"]) == (
|
|
second_run.run_id,
|
|
), second_list.text
|
|
|
|
owner_update: Final = gateway.request(
|
|
"PATCH",
|
|
f"/v1/workflows/runs/{first_run.run_id}",
|
|
{"status": "completed"},
|
|
key=key_one,
|
|
)
|
|
assert owner_update.status_code == 200, owner_update.text
|
|
assert WorkflowRunResponse.model_validate_json(owner_update.content).status == "completed", owner_update.text
|
|
owner_event: Final = gateway.request(
|
|
"POST",
|
|
f"/v1/workflows/runs/{first_run.run_id}/events",
|
|
{"event_type": "step.started", "step_name": "owner-step", "data": {"attempt": 1}},
|
|
key=key_one,
|
|
)
|
|
assert owner_event.status_code == 200, owner_event.text
|
|
assert WorkflowEventResponse.model_validate_json(owner_event.content).sequence_number == 0, owner_event.text
|
|
owner_message: Final = gateway.request(
|
|
"POST",
|
|
f"/v1/workflows/runs/{first_run.run_id}/messages",
|
|
{"role": "assistant", "content": "owner message", "session_id": "synthetic-session"},
|
|
key=key_one,
|
|
)
|
|
assert owner_message.status_code == 200, owner_message.text
|
|
assert WorkflowMessageResponse.model_validate_json(owner_message.content).content == "owner message", (
|
|
owner_message.text
|
|
)
|
|
assert _run_rows(first_run.run_id)[0]["status"] == "running"
|
|
|
|
owner_events: Final = gateway.request(
|
|
"GET",
|
|
f"/v1/workflows/runs/{first_run.run_id}/events",
|
|
key=key_one,
|
|
)
|
|
owner_messages: Final = gateway.request(
|
|
"GET",
|
|
f"/v1/workflows/runs/{first_run.run_id}/messages",
|
|
key=key_one,
|
|
)
|
|
assert owner_events.status_code == 200, owner_events.text
|
|
assert owner_messages.status_code == 200, owner_messages.text
|
|
assert object_value(owner_events.json())["count"] == 1, owner_events.text
|
|
assert object_value(owner_messages.json())["count"] == 1, owner_messages.text
|
|
|
|
admin_list: Final = gateway.request(
|
|
"GET",
|
|
"/v1/workflows/runs",
|
|
params={"workflow_type": workflow_type},
|
|
)
|
|
assert admin_list.status_code == 200, admin_list.text
|
|
admin_runs: Final = tuple(
|
|
WorkflowRunResponse.model_validate(run) for run in object_value(admin_list.json())["runs"]
|
|
)
|
|
assert frozenset(run.run_id for run in admin_runs) == frozenset((first_run.run_id, second_run.run_id)), (
|
|
admin_list.text
|
|
)
|
|
for run_id in (first_run.run_id, second_run.run_id):
|
|
visible_to_admin: Final = gateway.request("GET", f"/v1/workflows/runs/{run_id}")
|
|
assert visible_to_admin.status_code == 200, visible_to_admin.text
|
|
assert WorkflowRunResponse.model_validate_json(visible_to_admin.content).run_id == run_id, (
|
|
visible_to_admin.text
|
|
)
|
|
admin_update: Final = gateway.request(
|
|
"PATCH",
|
|
f"/v1/workflows/runs/{second_run.run_id}",
|
|
{"status": "completed"},
|
|
)
|
|
assert admin_update.status_code == 200, admin_update.text
|
|
assert _run_rows(second_run.run_id)[0]["status"] == "completed"
|