mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-02 07:13:02 +00:00
* test(integration): give chaos and timing tests a harness API and gate raw signals Chaos tests killed and suspended proxy workers with raw os.kill and psutil calls and bounded elapsed time with fixed literals, so they raced the uvicorn worker healthcheck and failed on slow CI boxes OwnedProxy now offers kill_worker and replacing_worker, which return only once a replacement answers readiness with its own pid, and suspended, which is refused unless the proxy was launched with a lenient worker healthcheck. timing.py bounds durations against a measured control or the injected delay. run.py measures the proxy import time once and derives readiness deadlines and the per-test timeout from it, writing boot-calibration.json check_test_quality.py adds TQ010 (raw process signals) and TQ011 (elapsed time asserted under a numeric literal) for tests/integration outside _support, and every call site is migrated so both report zero with no suppressions * test(integration): steer the assistants wire burst onto both workers The mixed-status burst assumed the OS would spread 48 connections across both workers and failed when one worker took them all. Each half is now sent while the other worker is suspended, and the test asserts an exact 24 and 24 split * test(integration): restore product-enforced timing bounds lost in the migration Some TQ011 migrations dropped or loosened a bound whose time is the behaviour under test. The websocket session cap only kept its floor, so a cap closing at 300 s would have passed, and several timeouts were bounded by the injected stall instead of the configured product value assert_enforced_within bounds an elapsed time by the configured value plus explicit slack, with an optional floor. The websocket cap, the MCP validation timeout, the guardrail litellm_params timeout, the prisma command timeouts, the baseline projection lock timeout and the key metadata statement timeout use it. The device code refusal keeps its 15 s margin, and the langfuse retried prompt fetch is now bounded by a cold fetch of a steady prompt as well as by the ignored retry-after * fix(tests): wait for the supervisor to reap killed workers and fix real-kill migrations The lenient worker healthcheck was the whole case budget, so uvicorn's supervisor sat in a ping on the killed worker for up to that long before respawning it. It now uses the graceful stop budget, and a worker kill returns only after the victim is reaped, so pooled connections no longer race the dead worker's socket. Several tests on main picked the multiprocessing resource tracker as their victim and never killed a worker. Now that they kill a real one, presidio counts streams cut by the death, delete_model waits for the config reload that removes a deleted model from a replacement worker, and the wedged coordination Redis test kills before the wedge and waits for every parked publish to fail before resuming Redis. The redis cluster test imports the harness through the tests package so it collects in the unit job, and the per-model fallback rig exposes the owned proxy it now needs. * test(observability): only require S3 deliveries the killed worker could not have buffered A real SIGKILL drops the victim's unflushed batch, so requests it already answered can't be expected to land. The test now requires every request served after the replacement to land, some of the surviving worker's burst share to land, and nothing unsent to land --------- Co-authored-by: Yuneng Jiang <yuneng@berri.ai>
1274 lines
56 KiB
Python
1274 lines
56 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import uuid
|
|
from collections.abc import Callable, Iterator, Mapping, Sequence
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from dataclasses import dataclass
|
|
from functools import partial
|
|
from itertools import product
|
|
from pathlib import Path
|
|
from types import MappingProxyType
|
|
from typing import Final, Protocol, TypeVar, cast
|
|
from urllib.parse import parse_qs
|
|
|
|
import anthropic
|
|
import httpx
|
|
import jwt
|
|
import openai
|
|
import psutil
|
|
import pytest
|
|
from cryptography.hazmat.primitives import serialization
|
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
from pydantic import JsonValue
|
|
|
|
from litellm.types.llms.anthropic import ANTHROPIC_OAUTH_TOKEN_PREFIX
|
|
from tests.integration._support.anthropic_thinking import (
|
|
answer,
|
|
identity,
|
|
marker_of,
|
|
message_body,
|
|
message_events,
|
|
prompt,
|
|
stream_reply,
|
|
streams,
|
|
text_events,
|
|
)
|
|
from tests.integration._support.client import (
|
|
JSON_OBJECT,
|
|
Gateway,
|
|
Scenario,
|
|
eventually,
|
|
gateway_from_environment,
|
|
object_value,
|
|
string_value,
|
|
)
|
|
from tests.integration._support.database import read_rows
|
|
from tests.integration._support.process import OwnedProxy, graceful_stop_seconds, owned_proxy_process
|
|
from tests.integration._support.wire import Reply, Request, Wire, wire_server
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
class _ClientNetworkStream(Protocol):
|
|
def get_extra_info(self, info: str) -> object | None: ...
|
|
|
|
|
|
SOURCES: Final = ("token_file", "secret_reference", "internal_issuer", "keycloak", "environment")
|
|
CLIENTS: Final = ("chat", "messages", "messages_stream")
|
|
|
|
_CLOSE: Final = MappingProxyType({"Connection": "close"})
|
|
_PROVIDER: Final = MappingProxyType({"custom_llm_provider": "anthropic"})
|
|
_SENSITIVE: Final = (
|
|
"authorization",
|
|
"token",
|
|
"key",
|
|
"secret",
|
|
"vertex_credentials",
|
|
"credentials",
|
|
"password",
|
|
"passwd",
|
|
)
|
|
_DEFAULT_TOKEN_FILE: Final = "/var/run/secrets/integration/anthropic-identity-token"
|
|
_DEFAULT_KEYCLOAK_URL: Final = "https://keycloak.integration.invalid/realms/integration/protocol/openid-connect/token"
|
|
_KEYCLOAK_TARGET: Final = "/realms/integration/protocol/openid-connect/token"
|
|
_KEYCLOAK_ASSERTION: Final = "keycloak-scripted-assertion"
|
|
_ISSUER: Final = "https://issuer.integration.invalid"
|
|
_SUBJECT: Final = "workload-integration"
|
|
_AUDIENCE: Final = "https://api.anthropic.com"
|
|
_TTL_SECONDS: Final = 900
|
|
_IDENTITY_TOKEN_VARIABLE: Final = "INTEGRATION_WIF_IDENTITY_TOKEN"
|
|
_SIGNING_KEY_VARIABLE: Final = "INTEGRATION_WIF_SIGNING_KEY"
|
|
_KEYCLOAK_SECRET_VARIABLE: Final = "INTEGRATION_WIF_KEYCLOAK_SECRET"
|
|
_JWT_BEARER: Final = "urn:ietf:params:oauth:grant-type:jwt-bearer"
|
|
_MODEL: Final = "anthropic/claude-haiku-4-5"
|
|
_CREDENTIAL_QUERY: Final = (
|
|
'SELECT credential_values, credential_info FROM "LiteLLM_CredentialsTable" WHERE credential_name = %s'
|
|
)
|
|
|
|
|
|
def _ids(rule_id: JsonValue) -> dict[str, JsonValue]:
|
|
return {
|
|
"anthropic_federation_rule_id": rule_id,
|
|
"anthropic_organization_id": "org-integration",
|
|
"anthropic_service_account_id": "svac-integration",
|
|
"anthropic_federation_workspace_id": "wrkspc-integration",
|
|
}
|
|
|
|
|
|
def _shape(
|
|
source: str,
|
|
rule_id: JsonValue,
|
|
*,
|
|
token_file: str = _DEFAULT_TOKEN_FILE,
|
|
keycloak_token_url: str = _DEFAULT_KEYCLOAK_URL,
|
|
) -> dict[str, JsonValue]:
|
|
match source:
|
|
case "token_file":
|
|
return {**_ids(rule_id), "anthropic_identity_token_file": token_file}
|
|
case "secret_reference":
|
|
return {**_ids(rule_id), "anthropic_identity_token": f"oidc/env/{_IDENTITY_TOKEN_VARIABLE}"}
|
|
case "internal_issuer":
|
|
return {
|
|
**_ids(rule_id),
|
|
"anthropic_identity_source": "internal_issuer",
|
|
"anthropic_issuer_url": _ISSUER,
|
|
"anthropic_issuer_subject": _SUBJECT,
|
|
"anthropic_issuer_audience": _AUDIENCE,
|
|
"anthropic_issuer_ttl_seconds": _TTL_SECONDS,
|
|
"anthropic_issuer_signing_key_ref": f"os.environ/{_SIGNING_KEY_VARIABLE}",
|
|
}
|
|
case "keycloak":
|
|
return {
|
|
**_ids(rule_id),
|
|
"anthropic_identity_source": "keycloak",
|
|
"anthropic_keycloak_token_url": keycloak_token_url,
|
|
"anthropic_keycloak_client_id": "litellm-integration",
|
|
"anthropic_keycloak_client_secret_ref": f"os.environ/{_KEYCLOAK_SECRET_VARIABLE}",
|
|
"anthropic_keycloak_auth_method": "client_secret_post",
|
|
"anthropic_keycloak_scope": "openid",
|
|
}
|
|
case "environment":
|
|
return _ids(rule_id)
|
|
case _:
|
|
pytest.fail(f"unknown identity source {source!r}")
|
|
|
|
|
|
def _sensitive(key: str) -> bool:
|
|
return any(word in key.lower() for word in _SENSITIVE)
|
|
|
|
|
|
def _masked(value: JsonValue) -> JsonValue:
|
|
if not isinstance(value, str):
|
|
return value
|
|
return "*****" if len(value) <= 4 else f"{value[:2]}****{value[-2:]}"
|
|
|
|
|
|
def _masked_view(values: Mapping[str, JsonValue]) -> dict[str, JsonValue]:
|
|
return {key: _masked(value) if _sensitive(key) else value for key, value in values.items()}
|
|
|
|
|
|
def _credential_name() -> str:
|
|
return f"federation-{uuid.uuid4().hex}"
|
|
|
|
|
|
def _create_body(name: str, values: Mapping[str, JsonValue]) -> dict[str, JsonValue]:
|
|
return {"credential_name": name, "credential_values": dict(values), "credential_info": dict(_PROVIDER)}
|
|
|
|
|
|
def _create(gateway: Gateway, scenario: Scenario, values: Mapping[str, JsonValue]) -> str:
|
|
name: Final = _credential_name()
|
|
scenario.cleanups.callback(_delete_if_present, gateway, name)
|
|
gateway.post("/credentials", _create_body(name, values))
|
|
return name
|
|
|
|
|
|
def _delete_if_present(gateway: Gateway, name: str) -> None:
|
|
response: Final = gateway.request("DELETE", f"/credentials/{name}")
|
|
assert response.status_code in (200, 404), response.text
|
|
|
|
|
|
def _patch(
|
|
gateway: Gateway,
|
|
name: str,
|
|
values: Mapping[str, JsonValue],
|
|
delete: Sequence[str] = (),
|
|
*,
|
|
key: str | None = None,
|
|
) -> httpx.Response:
|
|
return gateway.request(
|
|
"PATCH",
|
|
f"/credentials/{name}",
|
|
{
|
|
"credential_name": name,
|
|
"credential_values": dict(values),
|
|
"credential_info": dict(_PROVIDER),
|
|
"credential_values_to_delete": list(delete),
|
|
},
|
|
key=key,
|
|
)
|
|
|
|
|
|
def _stored_values(gateway: Gateway, name: str, *, key: str | None = None) -> tuple[int, JsonValue]:
|
|
response: Final = gateway.request("GET", f"/credentials/by_name/{name}", key=key, headers=_CLOSE)
|
|
if response.status_code != 200:
|
|
return response.status_code, response.text
|
|
return 200, JSON_OBJECT.validate_json(response.content)["credential_values"]
|
|
|
|
|
|
def _listed_values(gateway: Gateway, name: str) -> JsonValue:
|
|
response: Final = gateway.request("GET", "/credentials", headers=_CLOSE)
|
|
assert response.status_code == 200, response.text
|
|
listed: Final = JSON_OBJECT.validate_json(response.content)["credentials"]
|
|
assert isinstance(listed, list), listed
|
|
matching: Final = tuple(object_value(entry) for entry in listed if object_value(entry)["credential_name"] == name)
|
|
return matching[0]["credential_values"] if matching else None
|
|
|
|
|
|
def _stable(read: Callable[[], T], satisfied: Callable[[T], bool], *, seconds: float = 70) -> T:
|
|
def spread_over_workers() -> tuple[T, ...]:
|
|
with ThreadPoolExecutor(max_workers=10) as pool:
|
|
return tuple(pool.map(lambda _: read(), range(10)))
|
|
|
|
return eventually(
|
|
spread_over_workers,
|
|
lambda samples: all(satisfied(sample) for sample in samples),
|
|
seconds=seconds,
|
|
)[-1]
|
|
|
|
|
|
def _converged(gateway: Gateway, name: str, expected: Mapping[str, JsonValue], *, key: str | None = None) -> None:
|
|
masked: Final = _masked_view(expected)
|
|
_stable(partial(_stored_values, gateway, name, key=key), lambda sample: sample == (200, masked))
|
|
|
|
|
|
def _db_row(name: str) -> tuple[dict[str, JsonValue], dict[str, JsonValue]]:
|
|
rows: Final = read_rows(_CREDENTIAL_QUERY, (name,))
|
|
assert len(rows) == 1, rows
|
|
return object_value(rows[0]["credential_values"]), object_value(rows[0]["credential_info"])
|
|
|
|
|
|
def _assert_encrypted_at_rest(name: str, submitted: Mapping[str, JsonValue]) -> None:
|
|
stored, info = _db_row(name)
|
|
assert info == dict(_PROVIDER), info
|
|
assert stored.keys() == submitted.keys(), stored
|
|
dumped: Final = json.dumps(stored)
|
|
for key, value in submitted.items():
|
|
if not isinstance(value, str):
|
|
assert stored[key] == value, (key, stored[key])
|
|
continue
|
|
assert stored[key] != value, key
|
|
if _sensitive(key):
|
|
assert value not in dumped, key
|
|
|
|
|
|
def _without(values: Mapping[str, JsonValue], *keys: str) -> dict[str, JsonValue]:
|
|
return {key: value for key, value in values.items() if key not in keys}
|
|
|
|
|
|
def _listed_models(gateway: Gateway) -> frozenset[str]:
|
|
response: Final = gateway.request("GET", "/model/info", headers=_CLOSE)
|
|
assert response.status_code == 200, response.text
|
|
entries: Final = JSON_OBJECT.validate_json(response.content)["data"]
|
|
assert isinstance(entries, list), entries
|
|
return frozenset(string_value(object_value(entry)["model_name"]) for entry in entries)
|
|
|
|
|
|
def _deployments_visible(gateway: Gateway, names: Sequence[str]) -> None:
|
|
wanted: Final = frozenset(names)
|
|
_stable(partial(_listed_models, gateway), lambda listed: wanted <= listed)
|
|
|
|
|
|
def _federated_deployment(gateway: Gateway, scenario: Scenario, credential: str, api_base: str) -> str:
|
|
name: Final = f"integration-{uuid.uuid4().hex}"
|
|
created: Final = gateway.post(
|
|
"/model/new",
|
|
{
|
|
"model_name": name,
|
|
"litellm_params": {"model": _MODEL, "api_base": api_base, "litellm_credential_name": credential},
|
|
"model_info": {},
|
|
},
|
|
)
|
|
scenario.cleanups.callback(scenario.delete_model, string_value(object_value(created["model_info"])["id"]))
|
|
return name
|
|
|
|
|
|
def _chat(gateway: Gateway, model: str, marker: str) -> httpx.Response:
|
|
return gateway.request(
|
|
"POST",
|
|
"/v1/chat/completions",
|
|
{"model": model, "messages": [{"role": "user", "content": prompt(marker)}]},
|
|
headers=_CLOSE,
|
|
)
|
|
|
|
|
|
def _chat_outcome(gateway: Gateway, model: str) -> tuple[int, bool]:
|
|
marker: Final = uuid.uuid4().hex
|
|
response: Final = _chat(gateway, model, marker)
|
|
if response.status_code != 200:
|
|
return response.status_code, False
|
|
choices: Final = JSON_OBJECT.validate_json(response.content)["choices"]
|
|
assert isinstance(choices, list), choices
|
|
return 200, object_value(object_value(choices[0])["message"])["content"] == answer(marker)
|
|
|
|
|
|
@pytest.mark.parametrize("source", SOURCES)
|
|
def test_federation_shape_round_trips(gateway: Gateway, source: str) -> None:
|
|
with gateway.scenario() as scenario:
|
|
values: Final = _shape(source, f"fdrl-{uuid.uuid4().hex}")
|
|
name: Final = _create(gateway, scenario, values)
|
|
_converged(gateway, name, values)
|
|
_stable(partial(_listed_values, gateway, name), lambda listed: listed == _masked_view(values))
|
|
_assert_encrypted_at_rest(name, values)
|
|
|
|
|
|
def test_patch_sets_and_deletes_values(gateway: Gateway) -> None:
|
|
with gateway.scenario() as scenario:
|
|
values: Final = _shape("token_file", f"fdrl-{uuid.uuid4().hex}")
|
|
name: Final = _create(gateway, scenario, values)
|
|
_converged(gateway, name, values)
|
|
response: Final = _patch(
|
|
gateway,
|
|
name,
|
|
{"anthropic_federation_workspace_id": "wrkspc-updated"},
|
|
("anthropic_service_account_id",),
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
expected: Final = _without(
|
|
{**values, "anthropic_federation_workspace_id": "wrkspc-updated"}, "anthropic_service_account_id"
|
|
)
|
|
_converged(gateway, name, expected)
|
|
_assert_encrypted_at_rest(name, expected)
|
|
|
|
|
|
def test_patch_with_empty_values_only_deletes(gateway: Gateway) -> None:
|
|
with gateway.scenario() as scenario:
|
|
values: Final = _shape("token_file", f"fdrl-{uuid.uuid4().hex}")
|
|
name: Final = _create(gateway, scenario, values)
|
|
_converged(gateway, name, values)
|
|
response: Final = _patch(gateway, name, {}, ("anthropic_federation_workspace_id",))
|
|
assert response.status_code == 200, response.text
|
|
expected: Final = _without(values, "anthropic_federation_workspace_id")
|
|
_converged(gateway, name, expected)
|
|
_assert_encrypted_at_rest(name, expected)
|
|
|
|
|
|
def test_patch_overlapping_set_and_delete_is_refused(gateway: Gateway) -> None:
|
|
with gateway.scenario() as scenario:
|
|
values: Final = _shape("token_file", f"fdrl-{uuid.uuid4().hex}")
|
|
name: Final = _create(gateway, scenario, values)
|
|
_converged(gateway, name, values)
|
|
before: Final = _db_row(name)
|
|
response: Final = _patch(
|
|
gateway,
|
|
name,
|
|
{"anthropic_federation_workspace_id": "wrkspc-overlap"},
|
|
("anthropic_federation_workspace_id",),
|
|
)
|
|
assert response.status_code == 400, response.text
|
|
assert "credential_values_to_delete overlaps credential_values for key(s)" in response.text, response.text
|
|
assert "anthropic_federation_workspace_id" in response.text, response.text
|
|
assert _db_row(name) == before
|
|
_converged(gateway, name, values)
|
|
|
|
|
|
_MALFORMED: Final[Mapping[str, JsonValue]] = MappingProxyType(
|
|
{"string": "fdrl-malformed", "list": ["fdrl-malformed"], "empty": {}, "null": None}
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("shape", tuple(_MALFORMED))
|
|
def test_malformed_credential_values_are_refused(gateway: Gateway, shape: str) -> None:
|
|
name: Final = _credential_name()
|
|
with gateway.scenario() as scenario:
|
|
scenario.cleanups.callback(_delete_if_present, gateway, name)
|
|
response: Final = gateway.request(
|
|
"POST",
|
|
"/credentials",
|
|
{"credential_name": name, "credential_values": _MALFORMED[shape], "credential_info": dict(_PROVIDER)},
|
|
)
|
|
assert response.status_code == 422, response.text
|
|
assert read_rows(_CREDENTIAL_QUERY, (name,)) == []
|
|
assert _stored_values(gateway, name)[0] == 404
|
|
|
|
|
|
def test_oversized_and_non_string_ids_round_trip(gateway: Gateway) -> None:
|
|
with gateway.scenario() as scenario:
|
|
oversized: Final = _shape("environment", "f" * 5120)
|
|
numeric: Final = _shape("environment", 424242)
|
|
oversized_name: Final = _create(gateway, scenario, oversized)
|
|
numeric_name: Final = _create(gateway, scenario, numeric)
|
|
_converged(gateway, oversized_name, oversized)
|
|
_converged(gateway, numeric_name, numeric)
|
|
_assert_encrypted_at_rest(oversized_name, oversized)
|
|
_assert_encrypted_at_rest(numeric_name, numeric)
|
|
|
|
|
|
def test_duplicate_create_conflicts_and_repeated_patch_is_idempotent(gateway: Gateway) -> None:
|
|
with gateway.scenario() as scenario:
|
|
values: Final = _shape("token_file", f"fdrl-{uuid.uuid4().hex}")
|
|
name: Final = _create(gateway, scenario, values)
|
|
duplicate: Final = gateway.request(
|
|
"POST", "/credentials", _create_body(name, _shape("token_file", f"fdrl-{uuid.uuid4().hex}"))
|
|
)
|
|
assert duplicate.status_code == 409, duplicate.text
|
|
assert (
|
|
f"Credential '{name}' already exists. Update it with PATCH /credentials/{name}, or delete it first."
|
|
in duplicate.text
|
|
), duplicate.text
|
|
_converged(gateway, name, values)
|
|
_assert_encrypted_at_rest(name, values)
|
|
statuses: Final = tuple(
|
|
_patch(gateway, name, {"anthropic_federation_workspace_id": "wrkspc-twice"}).status_code for _ in range(2)
|
|
)
|
|
assert statuses == (200, 200), statuses
|
|
expected: Final = {**values, "anthropic_federation_workspace_id": "wrkspc-twice"}
|
|
_converged(gateway, name, expected)
|
|
_assert_encrypted_at_rest(name, expected)
|
|
|
|
|
|
@pytest.mark.parametrize("grant", ("routed", "plain"))
|
|
def test_non_admin_cannot_write_federation_fields(gateway: Gateway, grant: str) -> None:
|
|
with gateway.scenario() as scenario:
|
|
values: Final = _shape("token_file", f"fdrl-{uuid.uuid4().hex}")
|
|
name: Final = _create(gateway, scenario, values)
|
|
_converged(gateway, name, values)
|
|
before: Final = _db_row(name)
|
|
model: Final = scenario.model()
|
|
user_id: Final = scenario.user(user_role="internal_user")
|
|
key: Final = (
|
|
scenario.key(user_id=user_id, models=[model], allowed_routes=["/credentials*", "/v1/chat/completions"])
|
|
if grant == "routed"
|
|
else scenario.key(user_id=user_id, models=[model])
|
|
)
|
|
intruder: Final = _credential_name()
|
|
scenario.cleanups.callback(_delete_if_present, gateway, intruder)
|
|
attempts: Final = (
|
|
(
|
|
"create",
|
|
gateway.request(
|
|
"POST",
|
|
"/credentials",
|
|
_create_body(intruder, _shape("token_file", f"fdrl-{uuid.uuid4().hex}")),
|
|
key=key,
|
|
),
|
|
),
|
|
("set", _patch(gateway, name, {"anthropic_federation_rule_id": "fdrl-hijacked"}, key=key)),
|
|
("unset", _patch(gateway, name, {}, ("anthropic_identity_token_file",), key=key)),
|
|
("delete", gateway.request("DELETE", f"/credentials/{name}", key=key)),
|
|
("jwks", gateway.request("GET", f"/credentials/{name}/jwks", key=key)),
|
|
)
|
|
refused: Final = 403 if grant == "routed" else 401
|
|
assert tuple((label, response.status_code) for label, response in attempts) == tuple(
|
|
(label, refused) for label, _ in attempts
|
|
), tuple((label, response.text) for label, response in attempts)
|
|
if grant == "routed":
|
|
assert all("Only proxy admins" in response.text for _, response in attempts), tuple(
|
|
response.text for _, response in attempts
|
|
)
|
|
listing: Final = gateway.request("GET", "/credentials", key=key, headers=_CLOSE)
|
|
assert listing.status_code == (200 if grant == "routed" else 401), listing.text
|
|
assert _DEFAULT_TOKEN_FILE not in listing.text, listing.text
|
|
if grant == "routed":
|
|
_converged(gateway, name, values, key=key)
|
|
else:
|
|
assert _stored_values(gateway, name, key=key)[0] == 401
|
|
assert _db_row(name) == before
|
|
assert read_rows(_CREDENTIAL_QUERY, (intruder,)) == []
|
|
_converged(gateway, name, values)
|
|
gateway.chat(model, key=key)
|
|
|
|
|
|
def test_untrusted_exchange_host_is_refused_before_any_exchange(gateway: Gateway) -> None:
|
|
with wire_server(lambda request: Reply(status=500, body=b'{"error": "never reached"}')) as wire:
|
|
with gateway.scenario() as scenario:
|
|
values: Final = _shape("token_file", f"fdrl-{uuid.uuid4().hex}")
|
|
name: Final = _create(gateway, scenario, values)
|
|
model: Final = _federated_deployment(gateway, scenario, name, wire.url)
|
|
_deployments_visible(gateway, (model,))
|
|
response: Final = _chat(gateway, model, uuid.uuid4().hex)
|
|
assert response.status_code == 401, response.text
|
|
port: Final = httpx.URL(wire.url).port
|
|
assert "refused to use host '" in response.text, response.text
|
|
assert f":{port}'" in response.text, response.text
|
|
assert "LITELLM_ANTHROPIC_WIF_ALLOWED_HOSTS" in response.text, response.text
|
|
assert wire.drain() == ()
|
|
assert wire.connections() == 0
|
|
|
|
|
|
def test_concurrent_credential_writes_converge_across_workers(gateway: Gateway) -> None:
|
|
with gateway.scenario() as scenario:
|
|
names: Final = tuple(_credential_name() for _ in range(10))
|
|
for name in names:
|
|
scenario.cleanups.callback(_delete_if_present, gateway, name)
|
|
|
|
def lane(name: str) -> tuple[int, int, int, int]:
|
|
created: Final = gateway.request(
|
|
"POST", "/credentials", _create_body(name, _shape("token_file", f"fdrl-{name}"))
|
|
)
|
|
updated: Final = _patch(gateway, name, {"anthropic_federation_workspace_id": f"wrkspc-{name}"})
|
|
trimmed: Final = _patch(gateway, name, {}, ("anthropic_service_account_id",))
|
|
read: Final = gateway.request("GET", f"/credentials/by_name/{name}", headers=_CLOSE)
|
|
return created.status_code, updated.status_code, trimmed.status_code, read.status_code
|
|
|
|
with ThreadPoolExecutor(max_workers=10) as pool:
|
|
outcomes: Final = tuple(pool.map(lane, names))
|
|
assert all(outcome[:3] == (200, 200, 200) for outcome in outcomes), outcomes
|
|
assert all(outcome[3] in (200, 404) for outcome in outcomes), outcomes
|
|
for name in names:
|
|
expected: Final = _without(
|
|
{**_shape("token_file", f"fdrl-{name}"), "anthropic_federation_workspace_id": f"wrkspc-{name}"},
|
|
"anthropic_service_account_id",
|
|
)
|
|
_converged(gateway, name, expected)
|
|
_assert_encrypted_at_rest(name, expected)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class _Peer:
|
|
wire: Wire
|
|
seen: list[Request]
|
|
|
|
def requests(self) -> tuple[Request, ...]:
|
|
self.seen.extend(self.wire.drain())
|
|
return tuple(self.seen)
|
|
|
|
|
|
def _exchange_reply(request: Request) -> Reply:
|
|
grant: Final = JSON_OBJECT.validate_json(request.body)
|
|
minted: Final = {
|
|
"access_token": f"{ANTHROPIC_OAUTH_TOKEN_PREFIX}01-exchanged-{string_value(grant['federation_rule_id'])}",
|
|
"token_type": "Bearer",
|
|
"expires_in": 3600,
|
|
}
|
|
return Reply(body=json.dumps(minted).encode())
|
|
|
|
|
|
def _federation_peer(request: Request) -> Reply:
|
|
if request.target == "/v1/oauth/token":
|
|
return _exchange_reply(request)
|
|
if request.target == _KEYCLOAK_TARGET:
|
|
return Reply(
|
|
body=json.dumps({"access_token": _KEYCLOAK_ASSERTION, "token_type": "Bearer", "expires_in": 60}).encode()
|
|
)
|
|
marker: Final = marker_of(request)
|
|
if streams(request):
|
|
return stream_reply(request, message_events(marker, (text_events(0, answer(marker)),)))
|
|
return Reply(body=message_body(marker))
|
|
|
|
|
|
def _es256_private_key_pem() -> str:
|
|
return (
|
|
ec.generate_private_key(ec.SECP256R1())
|
|
.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption())
|
|
.decode()
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class _Secrets:
|
|
allowed_dir: Path
|
|
token_file: Path
|
|
identity_token: str
|
|
environment_token: str
|
|
keycloak_secret: str
|
|
signing_key_pem: str
|
|
|
|
def overrides(self) -> Mapping[str, str]:
|
|
return MappingProxyType(
|
|
{
|
|
"LITELLM_ANTHROPIC_WIF_ALLOWED_HOSTS": "127.0.0.1",
|
|
"LITELLM_OIDC_ALLOWED_CREDENTIAL_DIRS": str(self.allowed_dir),
|
|
"PROXY_CONFIG_RELOAD_INTERVAL_SECONDS": "2",
|
|
_IDENTITY_TOKEN_VARIABLE: self.identity_token,
|
|
_SIGNING_KEY_VARIABLE: self.signing_key_pem,
|
|
_KEYCLOAK_SECRET_VARIABLE: self.keycloak_secret,
|
|
"ANTHROPIC_IDENTITY_TOKEN": self.environment_token,
|
|
}
|
|
)
|
|
|
|
|
|
_REMOVED_ENVIRONMENT: Final = (
|
|
"ANTHROPIC_IDENTITY_TOKEN_FILE",
|
|
"ANTHROPIC_API_KEY",
|
|
"ANTHROPIC_API_BASE",
|
|
"ANTHROPIC_FEDERATION_RULE_ID",
|
|
"ANTHROPIC_ORGANIZATION_ID",
|
|
)
|
|
|
|
|
|
def _secrets(directory: Path) -> _Secrets:
|
|
allowed: Final = directory / "secrets"
|
|
allowed.mkdir()
|
|
token_file: Final = allowed / "anthropic-identity-token"
|
|
token_file.write_text(f"file-token-{uuid.uuid4().hex}")
|
|
return _Secrets(
|
|
allowed_dir=allowed,
|
|
token_file=token_file,
|
|
identity_token=f"env-token-{uuid.uuid4().hex}",
|
|
environment_token=f"ambient-token-{uuid.uuid4().hex}",
|
|
keycloak_secret=f"keycloak-secret-{uuid.uuid4().hex}",
|
|
signing_key_pem=_es256_private_key_pem(),
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class FederationRig:
|
|
owned: OwnedProxy
|
|
peer: _Peer
|
|
secrets: _Secrets
|
|
rule_ids: Mapping[str, str]
|
|
credentials: Mapping[str, str]
|
|
deployments: Mapping[str, str]
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def federation(tmp_path_factory: pytest.TempPathFactory) -> Iterator[FederationRig]:
|
|
directory: Final = tmp_path_factory.mktemp("federation").resolve()
|
|
secrets: Final = _secrets(directory)
|
|
with gateway_from_environment() as gateway, wire_server(_federation_peer) as wire:
|
|
with owned_proxy_process(
|
|
gateway, directory, secrets.overrides(), remove_environment=_REMOVED_ENVIRONMENT, workers=2
|
|
) as owned:
|
|
with owned.gateway.scenario() as scenario:
|
|
rule_ids: Final = {source: f"fdrl-{source}-{uuid.uuid4().hex}" for source in SOURCES}
|
|
credentials: Final = {
|
|
source: _create(
|
|
owned.gateway,
|
|
scenario,
|
|
_shape(
|
|
source,
|
|
rule_ids[source],
|
|
token_file=str(secrets.token_file),
|
|
keycloak_token_url=f"{wire.url}{_KEYCLOAK_TARGET}",
|
|
),
|
|
)
|
|
for source in SOURCES
|
|
}
|
|
deployments: Final = {
|
|
source: _federated_deployment(owned.gateway, scenario, credentials[source], wire.url)
|
|
for source in SOURCES
|
|
}
|
|
_deployments_visible(owned.gateway, tuple(deployments.values()))
|
|
yield FederationRig(
|
|
owned=owned,
|
|
peer=_Peer(wire, []),
|
|
secrets=secrets,
|
|
rule_ids=MappingProxyType(rule_ids),
|
|
credentials=MappingProxyType(credentials),
|
|
deployments=MappingProxyType(deployments),
|
|
)
|
|
|
|
|
|
def _call(rig: FederationRig, client: str, model: str, marker: str) -> None:
|
|
base_url: Final = str(rig.owned.gateway.client.base_url)
|
|
key: Final = rig.owned.gateway.key
|
|
match client:
|
|
case "chat":
|
|
completion: Final = openai.OpenAI(
|
|
base_url=base_url + "/v1", api_key=key, max_retries=0
|
|
).chat.completions.create(model=model, messages=[{"role": "user", "content": prompt(marker)}])
|
|
assert completion.choices[0].message.content == answer(marker), completion
|
|
case "messages":
|
|
message: Final = anthropic.Anthropic(base_url=base_url, api_key=key, max_retries=0).messages.create(
|
|
model=model, max_tokens=64, messages=[{"role": "user", "content": prompt(marker)}]
|
|
)
|
|
assert message.id == identity(marker), message
|
|
assert [block.text for block in message.content if block.type == "text"] == [answer(marker)], message
|
|
case "messages_stream":
|
|
with anthropic.Anthropic(base_url=base_url, api_key=key, max_retries=0).messages.stream(
|
|
model=model, max_tokens=64, messages=[{"role": "user", "content": prompt(marker)}]
|
|
) as stream:
|
|
final: Final = stream.get_final_message()
|
|
assert final.id == identity(marker), final
|
|
assert [block.text for block in final.content if block.type == "text"] == [answer(marker)], final
|
|
case _:
|
|
pytest.fail(f"unknown client {client!r}")
|
|
|
|
|
|
def _assert_assertion(rig: FederationRig, source: str, assertion: str, upstream: Sequence[Request]) -> None:
|
|
match source:
|
|
case "token_file":
|
|
assert assertion == rig.secrets.token_file.read_text()
|
|
case "secret_reference":
|
|
assert assertion == rig.secrets.identity_token
|
|
case "environment":
|
|
assert assertion == rig.secrets.environment_token
|
|
case "keycloak":
|
|
assert assertion == _KEYCLOAK_ASSERTION
|
|
grants: Final = tuple(request for request in upstream if request.target == _KEYCLOAK_TARGET)
|
|
assert grants, upstream
|
|
for grant in grants:
|
|
assert grant.headers.get("content-type") == "application/x-www-form-urlencoded", grant.headers
|
|
assert "authorization" not in grant.headers, grant.headers
|
|
assert parse_qs(grant.body.decode()) == {
|
|
"grant_type": ["client_credentials"],
|
|
"client_id": ["litellm-integration"],
|
|
"client_secret": [rig.secrets.keycloak_secret],
|
|
"scope": ["openid"],
|
|
}, grant.body
|
|
case "internal_issuer":
|
|
exported: Final = rig.owned.gateway.get(f"/credentials/{rig.credentials[source]}/jwks")["keys"]
|
|
assert isinstance(exported, list) and len(exported) == 1, exported
|
|
jwk: Final = object_value(exported[0])
|
|
header: Final = jwt.get_unverified_header(assertion)
|
|
assert (header["alg"], header["kid"]) == ("ES256", jwk["kid"]), (header, jwk)
|
|
claims: Final = jwt.decode(
|
|
assertion,
|
|
jwt.PyJWK(dict(jwk)).key,
|
|
algorithms=["ES256"],
|
|
audience=_AUDIENCE,
|
|
issuer=_ISSUER,
|
|
options={"verify_exp": False},
|
|
)
|
|
assert claims["sub"] == _SUBJECT, claims
|
|
assert claims["exp"] - claims["iat"] == _TTL_SECONDS, claims
|
|
assert claims["jti"], claims
|
|
case _:
|
|
pytest.fail(f"unknown identity source {source!r}")
|
|
|
|
|
|
@pytest.mark.timeout(240)
|
|
@pytest.mark.parametrize("client", CLIENTS)
|
|
@pytest.mark.parametrize("source", SOURCES)
|
|
def test_federated_exchange(federation: FederationRig, source: str, client: str) -> None:
|
|
marker: Final = uuid.uuid4().hex
|
|
rule_id: Final = federation.rule_ids[source]
|
|
_call(federation, client, federation.deployments[source], marker)
|
|
upstream: Final = federation.peer.requests()
|
|
sent: Final = tuple(
|
|
request for request in upstream if request.target == "/v1/messages" and marker in request.body.decode()
|
|
)
|
|
assert len(sent) == 1, upstream
|
|
assert sent[0].headers.get("authorization") == f"Bearer {ANTHROPIC_OAUTH_TOKEN_PREFIX}01-exchanged-{rule_id}", sent[
|
|
0
|
|
].headers
|
|
assert "oauth-2025-04-20" in sent[0].headers.get("anthropic-beta", ""), sent[0].headers
|
|
assert "x-api-key" not in sent[0].headers, sent[0].headers
|
|
exchanges: Final = tuple(
|
|
JSON_OBJECT.validate_json(request.body) for request in upstream if request.target == "/v1/oauth/token"
|
|
)
|
|
mine: Final = tuple(grant for grant in exchanges if grant["federation_rule_id"] == rule_id)
|
|
assert mine, exchanges
|
|
for grant in mine:
|
|
assert grant["grant_type"] == _JWT_BEARER, grant
|
|
assert (grant["organization_id"], grant["service_account_id"], grant["workspace_id"]) == (
|
|
"org-integration",
|
|
"svac-integration",
|
|
"wrkspc-integration",
|
|
), grant
|
|
_assert_assertion(federation, source, string_value(grant["assertion"]), upstream)
|
|
|
|
|
|
@pytest.mark.timeout(240)
|
|
def test_token_file_outside_allowed_dirs_is_refused_before_any_exchange(
|
|
federation: FederationRig, tmp_path: Path
|
|
) -> None:
|
|
stray: Final = tmp_path.resolve() / "anthropic-identity-token"
|
|
stray.write_text("stray-token")
|
|
rule_id: Final = f"fdrl-stray-{uuid.uuid4().hex}"
|
|
marker: Final = uuid.uuid4().hex
|
|
owned: Final = federation.owned
|
|
with owned.gateway.scenario() as scenario:
|
|
name: Final = _create(owned.gateway, scenario, _shape("token_file", rule_id, token_file=str(stray)))
|
|
model: Final = _federated_deployment(owned.gateway, scenario, name, federation.peer.wire.url)
|
|
_deployments_visible(owned.gateway, (model,))
|
|
response: Final = _chat(owned.gateway, model, marker)
|
|
assert response.status_code == 401, response.text
|
|
assert "LITELLM_OIDC_ALLOWED_CREDENTIAL_DIRS" in response.text, response.text
|
|
upstream: Final = federation.peer.requests()
|
|
assert not any(rule_id in request.body.decode() for request in upstream), upstream
|
|
assert not any(marker in request.body.decode() for request in upstream), upstream
|
|
assert owned.gateway.request("GET", "/health/liveliness").status_code == 200
|
|
control: Final = scenario.model()
|
|
assert _chat_outcome(owned.gateway, control)[0] == 200
|
|
|
|
|
|
def _is_worker(child: psutil.Process) -> bool:
|
|
try:
|
|
return "spawn_main" in " ".join(child.cmdline()) and child.status() != psutil.STATUS_ZOMBIE
|
|
except psutil.Error:
|
|
return False
|
|
|
|
|
|
def _workers(owned: OwnedProxy) -> tuple[psutil.Process, ...]:
|
|
return tuple(child for child in psutil.Process(owned.process.pid).children() if _is_worker(child))
|
|
|
|
|
|
def _worker_pids(owned: OwnedProxy) -> frozenset[int]:
|
|
return frozenset(child.pid for child in _workers(owned))
|
|
|
|
|
|
def _client_address(response: httpx.Response) -> object | None:
|
|
stream: Final = cast(_ClientNetworkStream | None, response.extensions.get("network_stream"))
|
|
return stream.get_extra_info("client_addr") if stream is not None else None
|
|
|
|
|
|
def _credential_read_on_worker(owned: OwnedProxy, name: str, _: int) -> int | None:
|
|
try:
|
|
with httpx.Client(
|
|
base_url=owned.gateway.client.base_url,
|
|
timeout=15,
|
|
trust_env=False,
|
|
limits=httpx.Limits(max_connections=1),
|
|
) as client:
|
|
before: Final = client.get("/health/integration-readiness")
|
|
credential: Final = client.get(
|
|
f"/credentials/by_name/{name}",
|
|
headers={"Authorization": f"Bearer {owned.gateway.key}"},
|
|
)
|
|
after: Final = client.get("/health/integration-readiness")
|
|
addresses: Final = tuple(_client_address(response) for response in (before, credential, after))
|
|
except (httpx.TransportError, OSError):
|
|
return None
|
|
before_pid: Final = cast(str | None, before.headers.get("x-integration-worker-pid")) or ""
|
|
after_pid: Final = cast(str | None, after.headers.get("x-integration-worker-pid")) or ""
|
|
if (
|
|
before.status_code != 200
|
|
or credential.status_code != 200
|
|
or after.status_code != 200
|
|
or not before_pid.isdecimal()
|
|
or not after_pid.isdecimal()
|
|
or before_pid != after_pid
|
|
or (
|
|
any(address is not None for address in addresses)
|
|
and not all(address == addresses[0] for address in addresses)
|
|
)
|
|
):
|
|
return None
|
|
return int(before_pid)
|
|
|
|
|
|
def _every_worker_holds(owned: OwnedProxy, name: str) -> bool:
|
|
with ThreadPoolExecutor(max_workers=8) as pool:
|
|
samples: Final = tuple(pool.map(partial(_credential_read_on_worker, owned, name), range(16)))
|
|
holders: Final = frozenset(pid for pid in samples if pid is not None)
|
|
workers: Final = _worker_pids(owned)
|
|
return len(workers) == 2 and workers <= holders
|
|
|
|
|
|
def _loaded_on_every_worker(owned: OwnedProxy, name: str) -> None:
|
|
assert eventually(lambda: _every_worker_holds(owned, name), lambda held: held, seconds=70)
|
|
|
|
|
|
@pytest.mark.timeout(240)
|
|
def test_worker_kill_mid_credential_burst(gateway: Gateway, tmp_path: Path) -> None:
|
|
directory: Final = tmp_path.resolve()
|
|
secrets: Final = _secrets(directory)
|
|
with wire_server(_federation_peer) as wire:
|
|
with owned_proxy_process(
|
|
gateway, directory, secrets.overrides(), remove_environment=_REMOVED_ENVIRONMENT, workers=2
|
|
) as owned:
|
|
with owned.gateway.scenario() as scenario:
|
|
rule_id: Final = f"fdrl-burst-{uuid.uuid4().hex}"
|
|
credential: Final = _create(
|
|
owned.gateway, scenario, _shape("token_file", rule_id, token_file=str(secrets.token_file))
|
|
)
|
|
model: Final = _federated_deployment(owned.gateway, scenario, credential, wire.url)
|
|
_deployments_visible(owned.gateway, (model,))
|
|
assert _chat_outcome(owned.gateway, model) == (200, True)
|
|
names: Final = tuple(_credential_name() for _ in range(24))
|
|
for name in names:
|
|
scenario.cleanups.callback(_delete_if_present, owned.gateway, name)
|
|
|
|
def create(name: str) -> tuple[str, int | str]:
|
|
try:
|
|
response: Final = owned.gateway.request(
|
|
"POST", "/credentials", _create_body(name, _shape("token_file", f"fdrl-{name}"))
|
|
)
|
|
except httpx.TransportError as error:
|
|
return name, type(error).__name__
|
|
return name, response.status_code
|
|
|
|
victim: Final = eventually(lambda: _workers(owned), lambda workers: len(workers) == 2)[0]
|
|
with owned.replacing_worker(victim.pid) as kill, ThreadPoolExecutor(max_workers=8) as pool:
|
|
futures: Final = tuple(pool.submit(create, name) for name in names)
|
|
kill()
|
|
outcomes: Final = tuple(future.result() for future in futures)
|
|
assert all(status == 200 or isinstance(status, str) for _, status in outcomes), outcomes
|
|
landed: Final = tuple(name for name, status in outcomes if status == 200)
|
|
for name in landed:
|
|
assert len(read_rows(_CREDENTIAL_QUERY, (name,))) == 1, name
|
|
respawned: Final = eventually(
|
|
lambda: frozenset(worker.pid for worker in _workers(owned)),
|
|
lambda pids: len(pids) == 2 and victim.pid not in pids,
|
|
seconds=30,
|
|
)
|
|
assert f"Child process [{victim.pid}] died" in owned.log.read_text(), respawned
|
|
with httpx.Client(
|
|
base_url=owned.gateway.client.base_url,
|
|
timeout=15,
|
|
trust_env=False,
|
|
limits=httpx.Limits(max_keepalive_connections=0),
|
|
) as fresh:
|
|
survivor: Final = Gateway(fresh, owned.gateway.key, owned.gateway.upstream_url)
|
|
for name in landed:
|
|
_converged(survivor, name, _shape("token_file", f"fdrl-{name}"))
|
|
_stable(partial(_chat_outcome, survivor, model), lambda outcome: outcome == (200, True))
|
|
|
|
|
|
_FAIL_CLOSED_HINT: Final = "Settings > Workload identity"
|
|
_REFUSAL_CLIENTS: Final = (
|
|
"chat",
|
|
"chat_stream",
|
|
"chat_async",
|
|
"messages",
|
|
"messages_stream",
|
|
"responses",
|
|
"responses_stream",
|
|
)
|
|
_OWNED_PROXY_CELL_SECONDS: Final = 2 * graceful_stop_seconds() + 120
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class _Misconfiguration:
|
|
reference: str
|
|
missing: tuple[str, ...]
|
|
blank: bool
|
|
expected: str
|
|
|
|
def values(self, rig: FederationRig, tag: str) -> dict[str, JsonValue]:
|
|
token: Final = (
|
|
str(rig.secrets.token_file)
|
|
if self.reference == "anthropic_identity_token_file"
|
|
else f"oidc/env/{_IDENTITY_TOKEN_VARIABLE}"
|
|
)
|
|
ids: Final = {"anthropic_federation_rule_id": f"fdrl-{tag}", "anthropic_organization_id": f"org-{tag}"}
|
|
kept: Final = {key: value for key, value in ids.items() if key not in self.missing}
|
|
blanked: Final = {key: "" for key in self.missing} if self.blank else {}
|
|
return {self.reference: token, **kept, **blanked}
|
|
|
|
|
|
_MISCONFIGURED: Final[Mapping[str, _Misconfiguration]] = MappingProxyType(
|
|
{
|
|
"token_file_without_org": _Misconfiguration(
|
|
"anthropic_identity_token_file",
|
|
("anthropic_organization_id",),
|
|
False,
|
|
"anthropic_identity_token_file is set, but anthropic_organization_id is not set",
|
|
),
|
|
"token_file_without_rule": _Misconfiguration(
|
|
"anthropic_identity_token_file",
|
|
("anthropic_federation_rule_id",),
|
|
False,
|
|
"anthropic_identity_token_file is set, but anthropic_federation_rule_id is not set",
|
|
),
|
|
"inline_token_without_org": _Misconfiguration(
|
|
"anthropic_identity_token",
|
|
("anthropic_organization_id",),
|
|
False,
|
|
"anthropic_identity_token is set, but anthropic_organization_id is not set",
|
|
),
|
|
"inline_token_without_rule": _Misconfiguration(
|
|
"anthropic_identity_token",
|
|
("anthropic_federation_rule_id",),
|
|
False,
|
|
"anthropic_identity_token is set, but anthropic_federation_rule_id is not set",
|
|
),
|
|
"token_file_without_ids": _Misconfiguration(
|
|
"anthropic_identity_token_file",
|
|
("anthropic_federation_rule_id", "anthropic_organization_id"),
|
|
False,
|
|
"anthropic_identity_token_file is set, but anthropic_federation_rule_id and anthropic_organization_id"
|
|
" are not set",
|
|
),
|
|
"token_file_blank_org": _Misconfiguration(
|
|
"anthropic_identity_token_file",
|
|
("anthropic_organization_id",),
|
|
True,
|
|
"anthropic_identity_token_file is set, but anthropic_organization_id is not set",
|
|
),
|
|
}
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class _Misconfigured:
|
|
tag: str
|
|
credential: str
|
|
model: str
|
|
shape: _Misconfiguration
|
|
|
|
|
|
def _misconfigured(rig: FederationRig, scenario: Scenario, shape: _Misconfiguration) -> _Misconfigured:
|
|
tag: Final = uuid.uuid4().hex
|
|
credential: Final = _create(rig.owned.gateway, scenario, shape.values(rig, tag))
|
|
model: Final = _federated_deployment(rig.owned.gateway, scenario, credential, rig.peer.wire.url)
|
|
return _Misconfigured(tag=tag, credential=credential, model=model, shape=shape)
|
|
|
|
|
|
async def _async_chat(base_url: str, key: str, model: str, marker: str) -> None:
|
|
await openai.AsyncOpenAI(base_url=base_url + "/v1", api_key=key, max_retries=0).chat.completions.create(
|
|
model=model, messages=[{"role": "user", "content": prompt(marker)}]
|
|
)
|
|
|
|
|
|
def _attempt(rig: FederationRig, client: str, model: str, marker: str) -> None:
|
|
base_url: Final = str(rig.owned.gateway.client.base_url)
|
|
key: Final = rig.owned.gateway.key
|
|
chat: Final = openai.OpenAI(base_url=base_url + "/v1", api_key=key, max_retries=0)
|
|
messages: Final = anthropic.Anthropic(base_url=base_url, api_key=key, max_retries=0)
|
|
match client:
|
|
case "chat":
|
|
chat.chat.completions.create(model=model, messages=[{"role": "user", "content": prompt(marker)}])
|
|
case "chat_stream":
|
|
for _ in chat.chat.completions.create(
|
|
model=model, messages=[{"role": "user", "content": prompt(marker)}], stream=True
|
|
):
|
|
pass
|
|
case "chat_async":
|
|
asyncio.run(_async_chat(base_url, key, model, marker))
|
|
case "messages":
|
|
messages.messages.create(model=model, max_tokens=64, messages=[{"role": "user", "content": prompt(marker)}])
|
|
case "messages_stream":
|
|
with messages.messages.stream(
|
|
model=model, max_tokens=64, messages=[{"role": "user", "content": prompt(marker)}]
|
|
) as stream:
|
|
stream.get_final_message()
|
|
case "responses":
|
|
chat.responses.create(model=model, input=prompt(marker))
|
|
case "responses_stream":
|
|
for _ in chat.responses.create(model=model, input=prompt(marker), stream=True):
|
|
pass
|
|
case _:
|
|
pytest.fail(f"unknown client {client!r}")
|
|
|
|
|
|
def _refusal(rig: FederationRig, client: str, model: str, marker: str) -> tuple[int, str]:
|
|
try:
|
|
_attempt(rig, client, model, marker)
|
|
except (openai.APIStatusError, anthropic.APIStatusError) as error:
|
|
return error.status_code, str(error)
|
|
pytest.fail(f"{client} succeeded against the misconfigured deployment {model}")
|
|
|
|
|
|
def _assert_names_missing_id(message: str, shape: _Misconfiguration) -> None:
|
|
assert shape.expected in message, message
|
|
assert _FAIL_CLOSED_HINT in message, message
|
|
|
|
|
|
def _assert_fails_closed(outcome: tuple[int, str], shape: _Misconfiguration) -> None:
|
|
status, message = outcome
|
|
assert status == 401, message
|
|
_assert_names_missing_id(message, shape)
|
|
|
|
|
|
def _assert_peer_untouched(rig: FederationRig, *fragments: str) -> None:
|
|
bodies: Final = tuple(request.body.decode() for request in rig.peer.requests())
|
|
touched: Final = tuple(body for body in bodies if any(fragment in body for fragment in fragments))
|
|
assert touched == (), touched
|
|
|
|
|
|
@pytest.mark.timeout(240)
|
|
@pytest.mark.parametrize("client", _REFUSAL_CLIENTS)
|
|
@pytest.mark.parametrize("shape", tuple(_MISCONFIGURED))
|
|
def test_legacy_reference_without_ids_fails_closed_before_any_exchange(
|
|
federation: FederationRig, shape: str, client: str
|
|
) -> None:
|
|
marker: Final = uuid.uuid4().hex
|
|
with federation.owned.gateway.scenario() as scenario:
|
|
misconfigured: Final = _misconfigured(federation, scenario, _MISCONFIGURED[shape])
|
|
_deployments_visible(federation.owned.gateway, (misconfigured.model,))
|
|
_assert_fails_closed(_refusal(federation, client, misconfigured.model, marker), misconfigured.shape)
|
|
_assert_peer_untouched(federation, misconfigured.tag, marker)
|
|
|
|
|
|
@pytest.mark.timeout(240)
|
|
def test_file_upload_fails_closed_naming_the_missing_id(federation: FederationRig) -> None:
|
|
note: Final = uuid.uuid4().hex
|
|
with federation.owned.gateway.scenario() as scenario:
|
|
misconfigured: Final = _misconfigured(federation, scenario, _MISCONFIGURED["token_file_without_org"])
|
|
_deployments_visible(federation.owned.gateway, (misconfigured.model,))
|
|
response: Final = federation.owned.gateway.request_multipart(
|
|
"/v1/files",
|
|
{"purpose": "user_data", "model": misconfigured.model},
|
|
{"file": ("notes.jsonl", json.dumps({"note": note}).encode(), "application/jsonl")},
|
|
)
|
|
_assert_fails_closed((response.status_code, response.text), misconfigured.shape)
|
|
_assert_peer_untouched(federation, misconfigured.tag, note)
|
|
|
|
|
|
@pytest.mark.timeout(240)
|
|
def test_skills_listing_fails_closed_naming_the_missing_id(federation: FederationRig) -> None:
|
|
with federation.owned.gateway.scenario() as scenario:
|
|
misconfigured: Final = _misconfigured(federation, scenario, _MISCONFIGURED["inline_token_without_rule"])
|
|
_deployments_visible(federation.owned.gateway, (misconfigured.model,))
|
|
response: Final = federation.owned.gateway.request(
|
|
"GET", "/v1/skills", params={"beta": "true", "model": misconfigured.model}, headers=_CLOSE
|
|
)
|
|
_assert_fails_closed((response.status_code, response.text), misconfigured.shape)
|
|
upstream: Final = federation.peer.requests()
|
|
listed: Final = tuple(request for request in upstream if request.target.startswith("/v1/skills"))
|
|
assert listed == (), listed
|
|
_assert_peer_untouched(federation, misconfigured.tag)
|
|
|
|
|
|
@pytest.mark.timeout(240)
|
|
def test_health_report_names_the_missing_id(federation: FederationRig) -> None:
|
|
with federation.owned.gateway.scenario() as scenario:
|
|
misconfigured: Final = _misconfigured(federation, scenario, _MISCONFIGURED["token_file_without_rule"])
|
|
_deployments_visible(federation.owned.gateway, (misconfigured.model,))
|
|
response: Final = federation.owned.gateway.request(
|
|
"GET", "/health", params={"model": misconfigured.model}, headers=_CLOSE
|
|
)
|
|
assert response.status_code == 503, response.text
|
|
unhealthy: Final = JSON_OBJECT.validate_json(response.content)["unhealthy_endpoints"]
|
|
assert isinstance(unhealthy, list) and len(unhealthy) == 1, response.text
|
|
_assert_names_missing_id(string_value(object_value(unhealthy[0])["error"]), misconfigured.shape)
|
|
_assert_peer_untouched(federation, misconfigured.tag)
|
|
|
|
|
|
@pytest.mark.timeout(240)
|
|
def test_connection_probe_names_the_missing_id(federation: FederationRig) -> None:
|
|
with federation.owned.gateway.scenario() as scenario:
|
|
misconfigured: Final = _misconfigured(federation, scenario, _MISCONFIGURED["inline_token_without_org"])
|
|
_deployments_visible(federation.owned.gateway, (misconfigured.model,))
|
|
_loaded_on_every_worker(federation.owned, misconfigured.credential)
|
|
response: Final = federation.owned.gateway.request(
|
|
"POST",
|
|
"/health/test_connection",
|
|
{
|
|
"litellm_params": {
|
|
"model": _MODEL,
|
|
"api_base": federation.peer.wire.url,
|
|
"litellm_credential_name": misconfigured.credential,
|
|
},
|
|
"mode": "chat",
|
|
},
|
|
headers=_CLOSE,
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
assert JSON_OBJECT.validate_json(response.content)["status"] == "error", response.text
|
|
_assert_names_missing_id(response.text, misconfigured.shape)
|
|
_assert_peer_untouched(federation, misconfigured.tag)
|
|
|
|
|
|
@pytest.mark.timeout(240)
|
|
def test_static_key_beside_a_stray_token_reference_still_wins(federation: FederationRig) -> None:
|
|
tag: Final = uuid.uuid4().hex
|
|
marker: Final = uuid.uuid4().hex
|
|
api_key: Final = f"sk-ant-api03-{tag}"
|
|
owned: Final = federation.owned
|
|
with owned.gateway.scenario() as scenario:
|
|
credential: Final = _create(
|
|
owned.gateway,
|
|
scenario,
|
|
{
|
|
"api_key": api_key,
|
|
"anthropic_identity_token_file": str(federation.secrets.token_file),
|
|
"anthropic_federation_rule_id": f"fdrl-{tag}",
|
|
},
|
|
)
|
|
model: Final = _federated_deployment(owned.gateway, scenario, credential, federation.peer.wire.url)
|
|
_deployments_visible(owned.gateway, (model,))
|
|
_call(federation, "chat", model, marker)
|
|
upstream: Final = federation.peer.requests()
|
|
sent: Final = tuple(
|
|
request for request in upstream if request.target == "/v1/messages" and marker in request.body.decode()
|
|
)
|
|
assert len(sent) == 1, upstream
|
|
assert sent[0].headers.get("x-api-key") == api_key, sent[0].headers
|
|
assert "authorization" not in sent[0].headers, sent[0].headers
|
|
_assert_peer_untouched(federation, tag)
|
|
|
|
|
|
@pytest.mark.timeout(240)
|
|
def test_blank_token_file_reference_is_unset_and_the_ambient_token_federates(federation: FederationRig) -> None:
|
|
rule_id: Final = f"fdrl-blank-{uuid.uuid4().hex}"
|
|
marker: Final = uuid.uuid4().hex
|
|
owned: Final = federation.owned
|
|
with owned.gateway.scenario() as scenario:
|
|
credential: Final = _create(owned.gateway, scenario, {**_ids(rule_id), "anthropic_identity_token_file": ""})
|
|
model: Final = _federated_deployment(owned.gateway, scenario, credential, federation.peer.wire.url)
|
|
_deployments_visible(owned.gateway, (model,))
|
|
_call(federation, "chat", model, marker)
|
|
grants: Final = tuple(
|
|
JSON_OBJECT.validate_json(request.body)
|
|
for request in federation.peer.requests()
|
|
if request.target == "/v1/oauth/token"
|
|
)
|
|
mine: Final = tuple(grant for grant in grants if grant["federation_rule_id"] == rule_id)
|
|
assert mine, grants
|
|
assert all(grant["assertion"] == federation.secrets.environment_token for grant in mine), mine
|
|
|
|
|
|
def test_unauthenticated_call_is_refused_before_the_credential_is_read(federation: FederationRig) -> None:
|
|
owned: Final = federation.owned
|
|
marker: Final = uuid.uuid4().hex
|
|
with owned.gateway.scenario() as scenario:
|
|
entry: Final = _misconfigured(federation, scenario, _MISCONFIGURED["token_file_without_org"])
|
|
_deployments_visible(owned.gateway, (entry.model,))
|
|
refused: Final = owned.gateway.request(
|
|
"POST",
|
|
"/v1/chat/completions",
|
|
{"model": entry.model, "messages": [{"role": "user", "content": prompt(marker)}]},
|
|
key=f"sk-not-a-key-{marker}",
|
|
headers=_CLOSE,
|
|
)
|
|
assert refused.status_code == 401, refused.text
|
|
assert _FAIL_CLOSED_HINT not in refused.text, refused.text
|
|
_assert_fails_closed(_refusal(federation, "chat", entry.model, uuid.uuid4().hex), entry.shape)
|
|
_assert_peer_untouched(federation, entry.tag, marker)
|
|
|
|
|
|
@pytest.mark.timeout(_OWNED_PROXY_CELL_SECONDS)
|
|
def test_environment_organization_id_completes_a_legacy_reference(gateway: Gateway, tmp_path: Path) -> None:
|
|
directory: Final = tmp_path.resolve()
|
|
secrets: Final = _secrets(directory)
|
|
tag: Final = uuid.uuid4().hex
|
|
organization: Final = f"org-env-{tag}"
|
|
rule_id: Final = f"fdrl-{tag}"
|
|
marker: Final = uuid.uuid4().hex
|
|
with wire_server(_federation_peer) as wire:
|
|
with owned_proxy_process(
|
|
gateway,
|
|
directory,
|
|
{**secrets.overrides(), "ANTHROPIC_ORGANIZATION_ID": organization},
|
|
remove_environment=_REMOVED_ENVIRONMENT,
|
|
workers=2,
|
|
) as owned:
|
|
with owned.gateway.scenario() as scenario:
|
|
credential: Final = _create(
|
|
owned.gateway,
|
|
scenario,
|
|
{"anthropic_identity_token_file": str(secrets.token_file), "anthropic_federation_rule_id": rule_id},
|
|
)
|
|
model: Final = _federated_deployment(owned.gateway, scenario, credential, wire.url)
|
|
_deployments_visible(owned.gateway, (model,))
|
|
response: Final = _chat(owned.gateway, model, marker)
|
|
assert response.status_code == 200, response.text
|
|
upstream: Final = wire.drain()
|
|
grants: Final = tuple(
|
|
JSON_OBJECT.validate_json(request.body)
|
|
for request in upstream
|
|
if request.target == "/v1/oauth/token"
|
|
)
|
|
mine: Final = tuple(grant for grant in grants if grant["federation_rule_id"] == rule_id)
|
|
assert mine, upstream
|
|
assert all(grant["organization_id"] == organization for grant in mine), mine
|
|
assert all(grant["assertion"] == secrets.token_file.read_text() for grant in mine), mine
|
|
|
|
|
|
@pytest.mark.timeout(240)
|
|
def test_mixed_burst_fails_closed_without_disturbing_healthy_federation(federation: FederationRig) -> None:
|
|
owned: Final = federation.owned
|
|
shapes: Final = tuple(_MISCONFIGURED.values())
|
|
healthy: Final = tuple(product(SOURCES, CLIENTS))
|
|
with owned.gateway.scenario() as scenario:
|
|
misconfigured: Final = tuple(
|
|
_misconfigured(federation, scenario, shapes[index % len(shapes)]) for index in range(12)
|
|
)
|
|
_deployments_visible(owned.gateway, tuple(entry.model for entry in misconfigured))
|
|
control: Final = scenario.model()
|
|
serial_markers: Final = tuple(uuid.uuid4().hex for _ in _REFUSAL_CLIENTS)
|
|
for client, marker in zip(_REFUSAL_CLIENTS, serial_markers, strict=True):
|
|
_assert_fails_closed(_refusal(federation, client, misconfigured[0].model, marker), misconfigured[0].shape)
|
|
refused_markers: Final = tuple(uuid.uuid4().hex for _ in misconfigured)
|
|
healthy_markers: Final = tuple(uuid.uuid4().hex for _ in healthy)
|
|
|
|
def refuse(index: int) -> tuple[int, str]:
|
|
client: Final = _REFUSAL_CLIENTS[index % len(_REFUSAL_CLIENTS)]
|
|
return _refusal(federation, client, misconfigured[index].model, refused_markers[index])
|
|
|
|
def federate(index: int) -> None:
|
|
source, client = healthy[index]
|
|
_call(federation, client, federation.deployments[source], healthy_markers[index])
|
|
|
|
with ThreadPoolExecutor(max_workers=16) as pool:
|
|
refusals: Final = tuple(pool.submit(refuse, index) for index in range(len(misconfigured)))
|
|
federations: Final = tuple(pool.submit(federate, index) for index in range(len(healthy)))
|
|
controls: Final = tuple(pool.submit(_chat_outcome, owned.gateway, control) for _ in range(4))
|
|
outcomes: Final = tuple(future.result() for future in refusals)
|
|
for future in federations:
|
|
future.result()
|
|
assert tuple(future.result()[0] for future in controls) == (200,) * 4
|
|
for entry, outcome in zip(misconfigured, outcomes, strict=True):
|
|
_assert_fails_closed(outcome, entry.shape)
|
|
sent: Final = tuple(
|
|
request.body.decode() for request in federation.peer.requests() if request.target == "/v1/messages"
|
|
)
|
|
for marker in healthy_markers:
|
|
assert sum(marker in body for body in sent) == 1, marker
|
|
_assert_peer_untouched(federation, *serial_markers, *refused_markers, *(entry.tag for entry in misconfigured))
|
|
assert owned.gateway.request("GET", "/health/liveliness").status_code == 200
|