mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-16 04:29:31 +00:00
* chore(typing): validate provider payloads at the boundary to clear 464 basedpyright Any errors Replaces untyped reads of decoded JSON in 48 provider, guardrail and proxy modules with one pydantic validation at the point the payload enters the module, so every value read afterwards carries a real type. reportAny 6111 -> 5663 and reportExplicitAny 1416 -> 1400. No basedpyright, ruff-strict or type-discipline rule is higher repo-wide or in any single file, and no cast, ignore or suppression marker was added. * chore(typing): replace 47 Any annotations that every caller already satisfies with object Each annotation here is a parameter or return type whose value is only stored, compared, logged or forwarded, so object describes it exactly and no function body changes. A site was kept only if no basedpyright, ruff-strict or type-discipline rule rose in any file that references it. reportExplicitAny 1400 -> 1353 and reportAny 5663 -> 5655. * chore(typing): type upstream responses and loose annotations across providers, proxy and integrations Second pass of the Any reduction. Decoded upstream JSON is validated once at the provider boundary and read through typed views afterwards, annotations that every caller already satisfies move from Any to a concrete type, and the nine search transformations validate each result item on its own so error text no longer carries the item index. No cast, no type: ignore, no suppression marker and no budget file edit * ci(unit): run the brave, google_pse, nlp_cloud and opensandbox tests in the providers shard
239 lines
6.9 KiB
Python
239 lines
6.9 KiB
Python
"""Tests for the HTTP command group."""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
from click.testing import CliRunner
|
|
|
|
|
|
|
|
import responses
|
|
|
|
from litellm.proxy.client.cli.commands.http import http
|
|
|
|
|
|
@pytest.fixture
|
|
def runner():
|
|
"""Create a CLI test runner."""
|
|
return CliRunner()
|
|
|
|
|
|
@responses.activate
|
|
def test_request_get(runner):
|
|
"""Test making a GET request."""
|
|
responses.add(
|
|
responses.GET,
|
|
"http://localhost:4000/models",
|
|
json={"models": []},
|
|
status=200,
|
|
)
|
|
result = runner.invoke(
|
|
http,
|
|
["request", "GET", "/models"],
|
|
obj={"base_url": "http://localhost:4000", "api_key": "sk-test-key"},
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "models" in result.output
|
|
|
|
|
|
@responses.activate
|
|
def test_request_post_with_json(runner):
|
|
"""Test making a POST request with JSON data."""
|
|
responses.add(
|
|
responses.POST,
|
|
"http://localhost:4000/chat/completions",
|
|
json={"choices": [{"message": {"content": "Hello!"}}]},
|
|
status=200,
|
|
)
|
|
result = runner.invoke(
|
|
http,
|
|
[
|
|
"request",
|
|
"POST",
|
|
"/chat/completions",
|
|
"-j",
|
|
'{"model": "gpt-4", "messages": [{"role": "user", "content": "Hello"}]}',
|
|
],
|
|
obj={"base_url": "http://localhost:4000", "api_key": "sk-test-key"},
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "choices" in result.output
|
|
|
|
|
|
@responses.activate
|
|
def test_request_with_headers(runner):
|
|
"""Test making a request with custom headers."""
|
|
responses.add(
|
|
responses.GET,
|
|
"http://localhost:4000/models",
|
|
json={"models": []},
|
|
status=200,
|
|
)
|
|
result = runner.invoke(
|
|
http,
|
|
[
|
|
"request",
|
|
"GET",
|
|
"/models",
|
|
"-H",
|
|
"X-Custom-Header:value",
|
|
"-H",
|
|
"Accept:application/json",
|
|
],
|
|
obj={"base_url": "http://localhost:4000", "api_key": "sk-test-key"},
|
|
)
|
|
assert result.exit_code == 0
|
|
assert "models" in result.output
|
|
|
|
|
|
def test_request_invalid_json(runner):
|
|
"""Test error handling for invalid JSON data."""
|
|
result = runner.invoke(
|
|
http,
|
|
[
|
|
"request",
|
|
"POST",
|
|
"/chat/completions",
|
|
"-j",
|
|
'{"invalid": json}', # Invalid JSON
|
|
],
|
|
obj={"base_url": "http://localhost:4000", "api_key": "sk-test-key"},
|
|
)
|
|
assert result.exit_code == 2 # Click error code for invalid parameter
|
|
assert "Invalid JSON format" in result.output
|
|
|
|
|
|
def test_request_invalid_header(runner):
|
|
"""Test error handling for invalid header format."""
|
|
result = runner.invoke(
|
|
http,
|
|
[
|
|
"request",
|
|
"GET",
|
|
"/models",
|
|
"-H",
|
|
"invalid-header", # Invalid header format
|
|
],
|
|
obj={"base_url": "http://localhost:4000", "api_key": "sk-test-key"},
|
|
)
|
|
assert result.exit_code == 2 # Click error code for invalid parameter
|
|
assert "Invalid header format" in result.output
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"raw_body",
|
|
[
|
|
'{"count": "12", "ratio": 1, "whole": 1.0, "enabled": "true", "flag": true, "missing": null}',
|
|
'{"data": [{"id": 1, "tags": ["a", 2, null, false]}, "two", null], "nested": {"deep": {"n": "007"}}}',
|
|
'[{"id": "1"}, 2, "3", null]',
|
|
'"plain string"',
|
|
"12",
|
|
"null",
|
|
"{}",
|
|
],
|
|
)
|
|
@responses.activate
|
|
def test_request_prints_the_server_json_unchanged(runner, raw_body):
|
|
responses.add(
|
|
responses.GET,
|
|
"http://localhost:4000/models",
|
|
body=raw_body,
|
|
content_type="application/json",
|
|
status=200,
|
|
)
|
|
result = runner.invoke(
|
|
http,
|
|
["request", "GET", "/models"],
|
|
obj={"base_url": "http://localhost:4000", "api_key": "sk-test-key"},
|
|
)
|
|
assert result.exit_code == 0
|
|
assert json.dumps(json.loads(result.stdout)) == json.dumps(json.loads(raw_body))
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"raw_body",
|
|
[
|
|
'{"error": {"message": "model not found", "code": "404", "status": 404, "retry": false}}',
|
|
'["first", {"detail": null}]',
|
|
'"denied"',
|
|
],
|
|
)
|
|
@responses.activate
|
|
def test_request_http_error_prints_the_json_error_body_unchanged(runner, raw_body):
|
|
responses.add(
|
|
responses.GET,
|
|
"http://localhost:4000/models",
|
|
body=raw_body,
|
|
content_type="application/json",
|
|
status=404,
|
|
)
|
|
result = runner.invoke(
|
|
http,
|
|
["request", "GET", "/models"],
|
|
obj={"base_url": "http://localhost:4000", "api_key": "sk-test-key"},
|
|
)
|
|
assert result.exit_code == 1
|
|
assert result.stderr.splitlines()[:1] == ["Error: HTTP 404"]
|
|
assert json.dumps(json.loads(result.stdout)) == json.dumps(json.loads(raw_body))
|
|
|
|
|
|
@responses.activate
|
|
def test_request_http_error_with_a_non_json_body_echoes_the_text(runner):
|
|
responses.add(
|
|
responses.GET,
|
|
"http://localhost:4000/models",
|
|
body="upstream exploded",
|
|
status=502,
|
|
)
|
|
result = runner.invoke(
|
|
http,
|
|
["request", "GET", "/models"],
|
|
obj={"base_url": "http://localhost:4000", "api_key": "sk-test-key"},
|
|
)
|
|
assert result.exit_code == 1
|
|
assert result.stdout == ""
|
|
assert result.stderr.splitlines()[:2] == ["Error: HTTP 502", "upstream exploded"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("api_key", "expected_authorization"),
|
|
[
|
|
("sk-test-key", "Bearer sk-test-key"),
|
|
(None, None),
|
|
("", None),
|
|
],
|
|
)
|
|
@responses.activate
|
|
def test_request_sends_the_context_api_key_to_the_context_base_url(runner, api_key, expected_authorization):
|
|
responses.add(responses.GET, "http://proxy.internal:4100/models", json={"models": []}, status=200)
|
|
result = runner.invoke(
|
|
http,
|
|
["request", "GET", "/models"],
|
|
obj={"base_url": "http://proxy.internal:4100/", "api_key": api_key},
|
|
)
|
|
assert result.exit_code == 0
|
|
assert len(responses.calls) == 1
|
|
assert responses.calls[0].request.url == "http://proxy.internal:4100/models"
|
|
assert responses.calls[0].request.headers.get("Authorization") == expected_authorization
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("obj", "missing_key"),
|
|
[
|
|
({"api_key": "sk-test-key"}, "base_url"),
|
|
({"base_url": "http://localhost:4000"}, "api_key"),
|
|
({}, "base_url"),
|
|
],
|
|
)
|
|
def test_request_with_an_incomplete_context_fails_on_the_first_missing_value(runner, obj, missing_key):
|
|
result = runner.invoke(http, ["request", "GET", "/models"], obj=obj)
|
|
assert result.exit_code == 1
|
|
assert type(result.exception) is KeyError
|
|
assert result.exception.args == (missing_key,)
|
|
|
|
|
|
def test_request_without_a_context_object_fails_instead_of_assuming_a_default_server(runner):
|
|
result = runner.invoke(http, ["request", "GET", "/models"], obj=None)
|
|
assert result.exit_code == 1
|
|
assert type(result.exception) is TypeError
|
|
assert result.output == ""
|