diff --git a/tests/e2e/coverage_registry/mgmt.yaml b/tests/e2e/coverage_registry/mgmt.yaml index 4fbaac0a2054..a43e71035239 100644 --- a/tests/e2e/coverage_registry/mgmt.yaml +++ b/tests/e2e/coverage_registry/mgmt.yaml @@ -66,3 +66,4 @@ - {id: mgmt.config_override.hashicorp_vault.happy_path, module: mgmt, tier: P2, surface: api, assertions: [happy_path], source: "config_override_endpoints.py", rationale: "Vault integration (smoke)"} - {id: mgmt.workflow.list.happy_path, module: mgmt, tier: P2, surface: api, assertions: [happy_path], source: "workflow_management_endpoints.py", rationale: "Workflow tracking (smoke)"} - {id: mgmt.credential_migration.check.happy_path, module: mgmt, tier: P2, surface: api, assertions: [happy_path], source: "key_management_endpoints.py:4252", rationale: "Encryption migration (smoke)"} +- {id: mgmt.credential.new.serves_request, module: mgmt, tier: P1, surface: api, assertions: [serves_request], source: "credential_endpoints/endpoints.py:42", rationale: "Stored credential resolves into a deployment and serves a live /messages request"} diff --git a/tests/e2e/llm_translation/test_credential_messages_e2e.py b/tests/e2e/llm_translation/test_credential_messages_e2e.py new file mode 100644 index 000000000000..49ea748430e7 --- /dev/null +++ b/tests/e2e/llm_translation/test_credential_messages_e2e.py @@ -0,0 +1,49 @@ +"""Live e2e: stored credentials resolve into a deployment serving /v1/messages.""" + +from __future__ import annotations + +import os + +import pytest + +from e2e_config import unique_marker +from e2e_http import require_successful_call +from endpoints_client import EndpointsClient, MessagesResult +from lifecycle import ResourceManager +from models import CredentialCreateBody, LiteLLMParamsBody + +pytestmark = pytest.mark.e2e + + +class TestCredentialBackedMessages: + @pytest.mark.covers("mgmt.credential.new.serves_request") + def test_credential_backed_messages(self, endpoints_client: EndpointsClient, resources: ResourceManager) -> None: + marker = unique_marker() + credential_name = f"e2e-cred-{marker}" + model = f"e2e-cred-messages-{marker}" + anthropic_api_key = os.getenv("ANTHROPIC_API_KEY") + assert anthropic_api_key, "ANTHROPIC_API_KEY must be set for this live e2e test" + + endpoints_client.proxy.create_credential( + CredentialCreateBody( + credential_name=credential_name, + credential_values={"api_key": anthropic_api_key}, + ) + ) + resources.defer(lambda: endpoints_client.proxy.delete_credential(credential_name)) + + model_id = endpoints_client.create_model( + model, + LiteLLMParamsBody( + model="anthropic/claude-haiku-4-5", + litellm_credential_name=credential_name, + ), + ) + resources.defer(lambda: endpoints_client.delete_model(model_id)) + + key = resources.key() + result = endpoints_client.messages(key, model, "reply with one word") + require_successful_call(result) + parsed = MessagesResult.model_validate_json(result.body) + assert parsed.role == "assistant", f"unexpected role: {result.body[:300]}" + assert parsed.text.strip(), f"/v1/messages returned no text: {result.body[:300]}" diff --git a/tests/e2e/models.py b/tests/e2e/models.py index 8d19e2f8965a..ebc0a122721c 100644 --- a/tests/e2e/models.py +++ b/tests/e2e/models.py @@ -492,6 +492,7 @@ class LiteLLMParamsBody(BaseModel): model: str api_key: str | None = None + litellm_credential_name: str | None = None api_base: str | None = None api_version: str | None = None realtime_protocol: str | None = None @@ -556,6 +557,16 @@ class ModelDeleteBody(BaseModel): id: str +class CredentialCreateBody(BaseModel): + credential_name: str + credential_values: dict[str, str] + credential_info: dict[str, str] = {} + + +class CredentialCreateResponse(BaseModel): + success: bool + + # ---------- key / team / user / organization management ---------- diff --git a/tests/e2e/proxy_client.py b/tests/e2e/proxy_client.py index 7eb860463757..f10392571936 100644 --- a/tests/e2e/proxy_client.py +++ b/tests/e2e/proxy_client.py @@ -31,6 +31,8 @@ ChatResponse, CountTokensBody, CountTokensResponse, + CredentialCreateBody, + CredentialCreateResponse, CustomerDeleteBody, EmbedBody, EmbedResponse, @@ -219,6 +221,26 @@ def delete_model(self, model_id: str) -> None: if not is_ok(result): warnings.warn(f"delete_model({model_id!r}) failed: {result}", stacklevel=2) + def create_credential(self, body: CredentialCreateBody) -> None: + unwrap( + self.transport.post( + "/credentials", + headers=self.transport.master, + json=body, + response_type=CredentialCreateResponse, + ) + ) + + def delete_credential(self, credential_name: str) -> None: + result = self.transport.delete( + f"/credentials/{credential_name}", + headers=self.transport.master, + json=NoBody(), + response_type=NoBody, + ) + if not is_ok(result): + warnings.warn(f"delete_credential({credential_name!r}) failed: {result}", stacklevel=2) + # ---- LLM calls ------------------------------------------------------ def chat(self, key: str, body: ChatBody) -> Result[ChatResponse]: