diff --git a/.env b/.env index 96e30a7..410fc9a 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -# This specifies the Invariant Explorer instance where the proxy will push the traces +# This specifies the Invariant Explorer instance where the gateway will push the traces # Set this to https://preview-explorer.invariantlabs.ai if you want to push to Preview. # If you want to push to a local instance of explorer, then specify the app-api docker container name like: # http://:8000 to push to the local explorer instance. diff --git a/.github/workflows/tests_ci.yml b/.github/workflows/tests_ci.yml index f5c135a..6e37b5a 100644 --- a/.github/workflows/tests_ci.yml +++ b/.github/workflows/tests_ci.yml @@ -1,4 +1,4 @@ -name: Invariant proxy testing CI +name: Invariant gateway testing CI on: push: diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 09cf7d4..4613534 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -1,10 +1,10 @@ services: - invariant-proxy: - container_name: invariant-proxy + invariant-gateway: + container_name: invariant-gateway build: - context: ./proxy - dockerfile: ../proxy/Dockerfile.proxy - working_dir: /srv/proxy + context: ./gateway + dockerfile: ../gateway/Dockerfile.gateway + working_dir: /srv/gateway env_file: - .env environment: @@ -12,8 +12,8 @@ services: - POLICIES_FILE_PATH=${POLICIES_FILE_PATH:+/srv/resources/policies.py} volumes: - type: bind - source: ./proxy - target: /srv/proxy + source: ./gateway + target: /srv/gateway - type: bind source: ${POLICIES_FILE_PATH:-/dev/null} target: /srv/resources/policies.py @@ -24,9 +24,9 @@ services: labels: # For access via Traefik - "traefik.enable=true" - - "traefik.http.routers.invariant-proxy-api.rule=(Host(`localhost`) && PathPrefix(`/api/v1/proxy/`)) || (Host(`127.0.0.1`) && PathPrefix(`/api/v1/proxy/`))" - - "traefik.http.routers.invariant-proxy-api.entrypoints=invariant-explorer-web" - - "traefik.http.services.invariant-proxy-api.loadbalancer.server.port=8000" + - "traefik.http.routers.invariant-gateway-api.rule=(Host(`localhost`) && PathPrefix(`/api/v1/gateway/`)) || (Host(`127.0.0.1`) && PathPrefix(`/api/v1/gateway/`))" + - "traefik.http.routers.invariant-gateway-api.entrypoints=invariant-explorer-web" + - "traefik.http.services.invariant-gateway-api.loadbalancer.server.port=8000" - "traefik.docker.network=invariant-explorer-web" networks: invariant-explorer-web: diff --git a/gateway/Dockerfile.gateway b/gateway/Dockerfile.gateway new file mode 100644 index 0000000..992aaad --- /dev/null +++ b/gateway/Dockerfile.gateway @@ -0,0 +1,12 @@ +FROM python:3.10 + +COPY ./requirements.txt /srv/gateway/requirements.txt +WORKDIR /srv/gateway + +RUN pip install --no-cache-dir -r requirements.txt +COPY . /srv/gateway + +# Ensure run.sh is executable +RUN chmod +x /srv/gateway/run.sh + +ENTRYPOINT ./run.sh \ No newline at end of file diff --git a/proxy/__init__.py b/gateway/__init__.py similarity index 100% rename from proxy/__init__.py rename to gateway/__init__.py diff --git a/proxy/common/__init__.py b/gateway/common/__init__.py similarity index 100% rename from proxy/common/__init__.py rename to gateway/common/__init__.py diff --git a/proxy/common/config_manager.py b/gateway/common/config_manager.py similarity index 78% rename from proxy/common/config_manager.py rename to gateway/common/config_manager.py index 811cc2e..09c5904 100644 --- a/proxy/common/config_manager.py +++ b/gateway/common/config_manager.py @@ -1,4 +1,4 @@ -"""Common Configurations for the Proxy Server.""" +"""Common Configurations for the Gateway Server.""" import os import threading @@ -6,8 +6,8 @@ import threading from invariant.analyzer import Policy -class ProxyConfig: - """Common configurations for the Proxy Server.""" +class GatewayConfig: + """Common configurations for the Gateway Server.""" def __init__(self): self.policies = self._load_policies() @@ -38,24 +38,24 @@ class ProxyConfig: raise ValueError(f"Invalid policy content in {policies_file}: {e}") from e def __repr__(self) -> str: - return f"ProxyConfig(policies={repr(self.policies)})" + return f"GatewayConfig(policies={repr(self.policies)})" -class ProxyConfigManager: - """Manager for Proxy Configuration.""" +class GatewayConfigManager: + """Manager for Gateway Configuration.""" _config_instance = None _lock = threading.Lock() @classmethod def get_config(cls): - """Initializes and returns the proxy configuration using double-checked locking.""" + """Initializes and returns the gateway configuration using double-checked locking.""" local_config = cls._config_instance if local_config is None: with cls._lock: local_config = cls._config_instance if local_config is None: - local_config = ProxyConfig() + local_config = GatewayConfig() cls._config_instance = local_config return local_config diff --git a/proxy/requirements.txt b/gateway/requirements.txt similarity index 100% rename from proxy/requirements.txt rename to gateway/requirements.txt diff --git a/proxy/routes/__init__.py b/gateway/routes/__init__.py similarity index 100% rename from proxy/routes/__init__.py rename to gateway/routes/__init__.py diff --git a/proxy/routes/anthropic.py b/gateway/routes/anthropic.py similarity index 97% rename from proxy/routes/anthropic.py rename to gateway/routes/anthropic.py index dfed21b..bb76fb3 100644 --- a/proxy/routes/anthropic.py +++ b/gateway/routes/anthropic.py @@ -1,10 +1,10 @@ -"""Proxy service to forward requests to the Anthropic APIs""" +"""Gateway service to forward requests to the Anthropic APIs""" import json from typing import Any, Optional import httpx -from common.config_manager import ProxyConfig, ProxyConfigManager +from common.config_manager import GatewayConfig, GatewayConfigManager from fastapi import APIRouter, Depends, Header, HTTPException, Request, Response from starlette.responses import StreamingResponse from utils.constants import ( @@ -14,7 +14,7 @@ from utils.constants import ( ) from utils.explorer import push_trace -proxy = APIRouter() +gateway = APIRouter() MISSING_INVARIANT_AUTH_API_KEY = "Missing invariant authorization header" MISSING_ANTHROPIC_AUTH_HEADER = "Missing Anthropic authorization header" @@ -37,18 +37,18 @@ def validate_headers(x_api_key: str = Header(None)): raise HTTPException(status_code=400, detail=MISSING_ANTHROPIC_AUTH_HEADER) -@proxy.post( +@gateway.post( "/{dataset_name}/anthropic/v1/messages", dependencies=[Depends(validate_headers)], ) -@proxy.post( +@gateway.post( "/anthropic/v1/messages", dependencies=[Depends(validate_headers)], ) -async def anthropic_v1_messages_proxy( +async def anthropic_v1_messages_gateway( request: Request, dataset_name: str = None, # This is None if the client doesn't want to push to Explorer - config: ProxyConfig = Depends(ProxyConfigManager.get_config), # pylint: disable=unused-argument + config: GatewayConfig = Depends(GatewayConfigManager.get_config), # pylint: disable=unused-argument ): """Proxy calls to the Anthropic APIs""" headers = { diff --git a/proxy/routes/gemini.py b/gateway/routes/gemini.py similarity index 84% rename from proxy/routes/gemini.py rename to gateway/routes/gemini.py index 7a5d42d..c60cb35 100644 --- a/proxy/routes/gemini.py +++ b/gateway/routes/gemini.py @@ -1,20 +1,19 @@ -"""Proxy service to forward requests to the Gemini APIs""" +"""Gateway service to forward requests to the Gemini APIs""" import json from typing import Any, Optional import httpx -from common.config_manager import ProxyConfig, ProxyConfigManager +from common.config_manager import GatewayConfig, GatewayConfigManager from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response from fastapi.responses import StreamingResponse from utils.constants import CLIENT_TIMEOUT, IGNORED_HEADERS -proxy = APIRouter() +gateway = APIRouter() -@proxy.post("/gemini/{api_version}/models/{model}:{endpoint}") -@proxy.post("/{dataset_name}/gemini/{api_version}/models/{model}:{endpoint}") -async def gemini_generate_content_proxy( +@gateway.post("/gemini/{api_version}/models/{model}:{endpoint}") +async def gemini_generate_content_gateway( request: Request, api_version: str, model: str, @@ -23,14 +22,14 @@ async def gemini_generate_content_proxy( alt: str = Query( None, title="Response Format", description="Set to 'sse' for streaming" ), - config: ProxyConfig = Depends(ProxyConfigManager.get_config), # pylint: disable=unused-argument + config: GatewayConfig = Depends(GatewayConfigManager.get_config), # pylint: disable=unused-argument ) -> Response: """Proxy calls to the Gemini GenerateContent API""" if endpoint not in ["generateContent", "streamGenerateContent"]: return Response( content="Invalid endpoint - the only endpoints supported are: \ - /api/v1/proxy/gemini//models/:generateContent or \ - /api/v1/proxy//gemini/models/:generateContent", + /api/v1/gateway/gemini//models/:generateContent or \ + /api/v1/gateway//gemini/models/:generateContent", status_code=400, ) headers = { @@ -83,7 +82,7 @@ async def stream_response( if not chunk_text: continue - # Yield chunk immediately to the client (proxy behavior) + # Yield chunk immediately to the client yield chunk # Send full merged response to the explorer diff --git a/proxy/routes/open_ai.py b/gateway/routes/open_ai.py similarity index 97% rename from proxy/routes/open_ai.py rename to gateway/routes/open_ai.py index 631f982..e04b154 100644 --- a/proxy/routes/open_ai.py +++ b/gateway/routes/open_ai.py @@ -1,10 +1,10 @@ -"""Proxy service to forward requests to the OpenAI APIs""" +"""Gateway service to forward requests to the OpenAI APIs""" import json from typing import Any, Optional import httpx -from common.config_manager import ProxyConfig, ProxyConfigManager +from common.config_manager import GatewayConfig, GatewayConfigManager from fastapi import APIRouter, Depends, Header, HTTPException, Request, Response from fastapi.responses import StreamingResponse from utils.constants import ( @@ -14,7 +14,7 @@ from utils.constants import ( ) from utils.explorer import push_trace -proxy = APIRouter() +gateway = APIRouter() MISSING_INVARIANT_AUTH_API_KEY = "Missing invariant api key" MISSING_AUTH_HEADER = "Missing authorization header" @@ -28,18 +28,18 @@ def validate_headers(authorization: str = Header(None)): raise HTTPException(status_code=400, detail=MISSING_AUTH_HEADER) -@proxy.post( +@gateway.post( "/{dataset_name}/openai/chat/completions", dependencies=[Depends(validate_headers)], ) -@proxy.post( +@gateway.post( "/openai/chat/completions", dependencies=[Depends(validate_headers)], ) -async def openai_chat_completions_proxy( +async def openai_chat_completions_gateway( request: Request, dataset_name: str = None, # This is None if the client doesn't want to push to Explorer - config: ProxyConfig = Depends(ProxyConfigManager.get_config), # pylint: disable=unused-argument + config: GatewayConfig = Depends(GatewayConfigManager.get_config), # pylint: disable=unused-argument ) -> Response: """Proxy calls to the OpenAI APIs""" headers = { @@ -153,7 +153,7 @@ async def stream_response( if not chunk_text: continue - # Yield chunk immediately to the client (proxy behavior) + # Yield chunk immediately to the client yield chunk # Process the chunk diff --git a/proxy/run.sh b/gateway/run.sh similarity index 100% rename from proxy/run.sh rename to gateway/run.sh diff --git a/gateway/serve.py b/gateway/serve.py new file mode 100644 index 0000000..5fcdf16 --- /dev/null +++ b/gateway/serve.py @@ -0,0 +1,36 @@ +"""Serve the API""" + +import fastapi +import uvicorn +from routes.anthropic import gateway as anthropic_gateway +from routes.gemini import gateway as gemini_gateway +from routes.open_ai import gateway as open_ai_gateway +from starlette_compress import CompressMiddleware + +app = fastapi.app = fastapi.FastAPI( + docs_url="/api/v1/gateway/docs", + redoc_url="/api/v1/gateway/redoc", + openapi_url="/api/v1/gateway/openapi.json", +) +app.add_middleware(CompressMiddleware) + +router = fastapi.APIRouter(prefix="/api/v1") + + +@router.get("/gateway/health", tags=["health_check"], include_in_schema=False) +async def check_health(): + """Health check""" + return {"message": "Hello from Invariant v1/gateway"} + + +router.include_router(open_ai_gateway, prefix="/gateway", tags=["open_ai_gateway"]) + +router.include_router(anthropic_gateway, prefix="/gateway", tags=["anthropic_gateway"]) + +router.include_router(gemini_gateway, prefix="/gateway", tags=["gemini_gateway"]) + +app.include_router(router) + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/proxy/utils/__init__.py b/gateway/utils/__init__.py similarity index 100% rename from proxy/utils/__init__.py rename to gateway/utils/__init__.py diff --git a/proxy/utils/constants.py b/gateway/utils/constants.py similarity index 87% rename from proxy/utils/constants.py rename to gateway/utils/constants.py index 12801c2..07434ff 100644 --- a/proxy/utils/constants.py +++ b/gateway/utils/constants.py @@ -1,4 +1,4 @@ -"""Common constants used in the proxy.""" +"""Common constants used in the gateway.""" IGNORED_HEADERS = [ "accept-encoding", diff --git a/proxy/utils/explorer.py b/gateway/utils/explorer.py similarity index 100% rename from proxy/utils/explorer.py rename to gateway/utils/explorer.py diff --git a/gateway/validate_config.py b/gateway/validate_config.py new file mode 100644 index 0000000..4d13c89 --- /dev/null +++ b/gateway/validate_config.py @@ -0,0 +1,13 @@ +"""Validates the GatewayConfigManager configuration.""" + +import sys + +from common.config_manager import GatewayConfigManager + +try: + _ = GatewayConfigManager.get_config() + print("GatewayConfig validated successfully.") + sys.exit(0) +except Exception as e: + print(f"Error loading GatewayConfig error: {e}") + sys.exit(1) diff --git a/proxy/Dockerfile.proxy b/proxy/Dockerfile.proxy deleted file mode 100644 index 0cc24c6..0000000 --- a/proxy/Dockerfile.proxy +++ /dev/null @@ -1,12 +0,0 @@ -FROM python:3.10 - -COPY ./requirements.txt /srv/proxy/requirements.txt -WORKDIR /srv/proxy - -RUN pip install --no-cache-dir -r requirements.txt -COPY . /srv/proxy - -# Ensure run.sh is executable -RUN chmod +x /srv/proxy/run.sh - -ENTRYPOINT ./run.sh \ No newline at end of file diff --git a/proxy/serve.py b/proxy/serve.py deleted file mode 100644 index d7503b4..0000000 --- a/proxy/serve.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Serve the API""" - -import fastapi -import uvicorn -from routes.anthropic import proxy as anthropic_proxy -from routes.gemini import proxy as gemini_proxy -from routes.open_ai import proxy as open_ai_proxy -from starlette_compress import CompressMiddleware - -app = fastapi.app = fastapi.FastAPI( - docs_url="/api/v1/proxy/docs", - redoc_url="/api/v1/proxy/redoc", - openapi_url="/api/v1/proxy/openapi.json", -) -app.add_middleware(CompressMiddleware) - -router = fastapi.APIRouter(prefix="/api/v1") - - -@router.get("/proxy/health", tags=["health_check"], include_in_schema=False) -async def get_proxy_info(): - """Health check""" - return {"message": "Hello from Invariant v1/proxy"} - - -router.include_router(open_ai_proxy, prefix="/proxy", tags=["open_ai_proxy"]) - -router.include_router(anthropic_proxy, prefix="/proxy", tags=["anthropic_proxy"]) - -router.include_router(gemini_proxy, prefix="/proxy", tags=["gemini_proxy"]) - -app.include_router(router) - - -if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/proxy/validate_config.py b/proxy/validate_config.py deleted file mode 100644 index 2b11a54..0000000 --- a/proxy/validate_config.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Validates the ProxyConfigManager configuration.""" - -import sys - -from common.config_manager import ProxyConfigManager - -try: - _ = ProxyConfigManager.get_config() - print("ProxyConfig validated successfully.") - sys.exit(0) -except Exception as e: - print(f"Error loading ProxyConfig error: {e}") - sys.exit(1) diff --git a/resources/images/invariant-proxy.png b/resources/images/invariant-gateway.png similarity index 100% rename from resources/images/invariant-proxy.png rename to resources/images/invariant-gateway.png diff --git a/run.sh b/run.sh index b50c94f..eea5c40 100755 --- a/run.sh +++ b/run.sh @@ -32,8 +32,8 @@ up() { # Start Docker Compose with the correct environment variable POLICIES_FILE_PATH="$POLICIES_FILE_PATH" docker compose -f docker-compose.local.yml up -d - echo "Proxy started at http://localhost:8005/api/v1/proxy/" - echo "See http://localhost:8005/api/v1/proxy/docs for API documentation" + echo "Gateway started at http://localhost:8005/api/v1/gateway/" + echo "See http://localhost:8005/api/v1/gateway/docs for API documentation" echo "Using Policies File: ${POLICIES_FILE_PATH:-None}" } @@ -53,11 +53,11 @@ tests() { echo "Setting up test environment..." # Ensure test network exists - docker network inspect invariant-proxy-web-test >/dev/null 2>&1 || \ - docker network create invariant-proxy-web-test + docker network inspect invariant-gateway-web-test >/dev/null 2>&1 || \ + docker network create invariant-gateway-web-test # Setup the explorer.test.yml file - CONFIG_DIR="/tmp/invariant-proxy-test/configs" + CONFIG_DIR="/tmp/invariant-gateway-test/configs" FILE="$CONFIG_DIR/explorer.test.yml" mkdir -p "$CONFIG_DIR" # Download the file @@ -74,13 +74,13 @@ tests() { docker compose -f tests/docker-compose.test.yml build docker compose -f tests/docker-compose.test.yml up -d - until [ "$(docker inspect -f '{{.State.Health.Status}}' invariant-proxy-test-explorer-app-api)" = "healthy" ]; do + until [ "$(docker inspect -f '{{.State.Health.Status}}' invariant-gateway-test-explorer-app-api)" = "healthy" ]; do echo "Explorer backend app-api instance container starting..." sleep 2 done - until [ "$(docker inspect -f '{{.State.Health.Status}}' invariant-proxy-test)" = "healthy" ]; do - echo "Invariant proxy test instance container starting..." + until [ "$(docker inspect -f '{{.State.Health.Status}}' invariant-gateway-test)" = "healthy" ]; do + echo "Invariant gateway test instance container starting..." sleep 2 done @@ -89,15 +89,15 @@ tests() { # Make call to signup endpoint curl -k -X POST http://127.0.0.1/api/v1/user/signup - docker build -t 'invariant-proxy-tests' -f ./tests/Dockerfile.test ./tests + docker build -t 'invariant-gateway-tests' -f ./tests/Dockerfile.test ./tests docker run \ --mount type=bind,source=./tests,target=/tests \ - --network invariant-proxy-web-test \ + --network invariant-gateway-web-test \ -e OPENAI_API_KEY="$OPENAI_API_KEY" \ -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY"\ --env-file ./tests/.env.test \ - invariant-proxy-tests $@ + invariant-gateway-tests $@ } # ----------------------------- diff --git a/tests/.env.test b/tests/.env.test index cb23d18..bf05a66 100644 --- a/tests/.env.test +++ b/tests/.env.test @@ -1,8 +1,8 @@ # To push traces to the local instance of the explorer app-api -# from the proxy. Both proxy and app-api are on the same network: -# invariant-proxy-web-test -INVARIANT_API_URL=http://invariant-proxy-test-explorer-app-api:8000 -INVARIANT_PROXY_API_URL=http://invariant-proxy-test:8000 +# from the gateway. Both gateway and app-api are on the same network: +# invariant-gateway-web-test +INVARIANT_API_URL=http://invariant-gateway-test-explorer-app-api:8000 +INVARIANT_GATEWAY_API_URL=http://invariant-gateway-test:8000 POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres diff --git a/tests/anthropic/test_anthropic_header_with_invariant_key.py b/tests/anthropic/test_anthropic_header_with_invariant_key.py index d5304c9..2a73cc0 100644 --- a/tests/anthropic/test_anthropic_header_with_invariant_key.py +++ b/tests/anthropic/test_anthropic_header_with_invariant_key.py @@ -1,4 +1,4 @@ -"""Test the Anthropic proxy with Invariant key in the ANTHROPIC_API_KEY.""" +"""Test the Anthropic gateway with Invariant key in the ANTHROPIC_API_KEY.""" import datetime import os @@ -19,10 +19,10 @@ pytest_plugins = ("pytest_asyncio",) @pytest.mark.skipif( not os.getenv("ANTHROPIC_API_KEY"), reason="No ANTHROPIC_API_KEY set" ) -async def test_proxy_with_invariant_key_in_anthropic_key_header( - context, proxy_url, explorer_api_url +async def test_gateway_with_invariant_key_in_anthropic_key_header( + context, gateway_url, explorer_api_url ): - """Test the Anthropic proxy with Invariant key in the Anthropic key""" + """Test the Anthropic gateway with Invariant key in the Anthropic key""" anthropic_api_key = os.getenv("ANTHROPIC_API_KEY") dataset_name = "claude_header_test" + str( datetime.datetime.now().strftime("%Y%m%d%H%M%S") @@ -36,7 +36,7 @@ async def test_proxy_with_invariant_key_in_anthropic_key_header( ): client = anthropic.Anthropic( http_client=Client(), - base_url=f"{proxy_url}/api/v1/proxy/{dataset_name}/anthropic", + base_url=f"{gateway_url}/api/v1/gateway/{dataset_name}/anthropic", ) response = client.messages.create( model="claude-3-5-sonnet-20241022", diff --git a/tests/anthropic/test_anthropic_with_tool_call.py b/tests/anthropic/test_anthropic_with_tool_call.py index 36f1982..12a25d8 100644 --- a/tests/anthropic/test_anthropic_with_tool_call.py +++ b/tests/anthropic/test_anthropic_with_tool_call.py @@ -22,7 +22,7 @@ pytest_plugins = ("pytest_asyncio",) class WeatherAgent: """Weather agent to get the current weather in a given location.""" - def __init__(self, proxy_url, push_to_explorer): + def __init__(self, gateway_url, push_to_explorer): self.dataset_name = "claude_weather_agent_test" + str( datetime.datetime.now().strftime("%Y%m%d%H%M%S") ) @@ -31,9 +31,9 @@ class WeatherAgent: http_client=Client( headers={"Invariant-Authorization": f"Bearer {invariant_api_key}"}, ), - base_url=f"{proxy_url}/api/v1/proxy/{self.dataset_name}/anthropic" + base_url=f"{gateway_url}/api/v1/gateway/{self.dataset_name}/anthropic" if push_to_explorer - else f"{proxy_url}/api/v1/proxy/anthropic", + else f"{gateway_url}/api/v1/gateway/anthropic", ) self.get_weather_function = { "name": "get_weather", @@ -181,11 +181,11 @@ class WeatherAgent: ) @pytest.mark.parametrize("push_to_explorer", [False, True]) async def test_response_with_tool_call( - context, explorer_api_url, proxy_url, push_to_explorer + context, explorer_api_url, gateway_url, push_to_explorer ): """Test the chat completion without streaming for the weather agent.""" - weather_agent = WeatherAgent(proxy_url, push_to_explorer) + weather_agent = WeatherAgent(gateway_url, push_to_explorer) query = "Tell me the weather for New York" @@ -242,10 +242,10 @@ async def test_response_with_tool_call( ) @pytest.mark.parametrize("push_to_explorer", [False, True]) async def test_streaming_response_with_tool_call( - context, explorer_api_url, proxy_url, push_to_explorer + context, explorer_api_url, gateway_url, push_to_explorer ): """Test the chat completion with streaming for the weather agent.""" - weather_agent = WeatherAgent(proxy_url, push_to_explorer) + weather_agent = WeatherAgent(gateway_url, push_to_explorer) query = "Tell me the weather for New York" city = "new york" @@ -297,10 +297,10 @@ async def test_streaming_response_with_tool_call( ) @pytest.mark.parametrize("push_to_explorer", [False, True]) async def test_response_with_tool_call_with_image( - context, explorer_api_url, proxy_url, push_to_explorer + context, explorer_api_url, gateway_url, push_to_explorer ): """Test the chat completion with image for the weather agent.""" - weather_agent = WeatherAgent(proxy_url, push_to_explorer) + weather_agent = WeatherAgent(gateway_url, push_to_explorer) image_path = Path(__file__).parent.parent / "images" / "new-york.jpeg" diff --git a/tests/anthropic/test_anthropic_without_tool_call.py b/tests/anthropic/test_anthropic_without_tool_call.py index cb991dc..53484c8 100644 --- a/tests/anthropic/test_anthropic_without_tool_call.py +++ b/tests/anthropic/test_anthropic_without_tool_call.py @@ -20,9 +20,9 @@ pytest_plugins = ("pytest_asyncio",) ) @pytest.mark.parametrize("push_to_explorer", [False, True]) async def test_response_without_tool_call( - context, explorer_api_url, proxy_url, push_to_explorer + context, explorer_api_url, gateway_url, push_to_explorer ): - """Test the Anthropic proxy without tool calling.""" + """Test the Anthropic gateway without tool calling.""" dataset_name = "claude_streaming_response_without_tool_call_test" + str( datetime.datetime.now().strftime("%Y%m%d%H%M%S") ) @@ -32,9 +32,9 @@ async def test_response_without_tool_call( http_client=Client( headers={"Invariant-Authorization": f"Bearer {invariant_api_key}"}, ), - base_url=f"{proxy_url}/api/v1/proxy/{dataset_name}/anthropic" + base_url=f"{gateway_url}/api/v1/gateway/{dataset_name}/anthropic" if push_to_explorer - else f"{proxy_url}/api/v1/proxy/anthropic", + else f"{gateway_url}/api/v1/gateway/anthropic", ) cities = ["zurich", "new york", "london"] @@ -82,9 +82,9 @@ async def test_response_without_tool_call( ) @pytest.mark.parametrize("push_to_explorer", [False, True]) async def test_streaming_response_without_tool_call( - context, explorer_api_url, proxy_url, push_to_explorer + context, explorer_api_url, gateway_url, push_to_explorer ): - """Test the Anthropic proxy without tool calling.""" + """Test the Anthropic gateway without tool calling.""" dataset_name = "claude_streaming_response_without_tool_call_test" + str( datetime.datetime.now().strftime("%Y%m%d%H%M%S") ) @@ -94,9 +94,9 @@ async def test_streaming_response_without_tool_call( http_client=Client( headers={"Invariant-Authorization": f"Bearer {invariant_api_key}"}, ), - base_url=f"{proxy_url}/api/v1/proxy/{dataset_name}/anthropic" + base_url=f"{gateway_url}/api/v1/gateway/{dataset_name}/anthropic" if push_to_explorer - else f"{proxy_url}/api/v1/proxy/anthropic", + else f"{gateway_url}/api/v1/gateway/anthropic", ) cities = ["zurich", "new york", "london"] diff --git a/tests/docker-compose.test.yml b/tests/docker-compose.test.yml index 9d012d0..e1c1275 100644 --- a/tests/docker-compose.test.yml +++ b/tests/docker-compose.test.yml @@ -1,8 +1,8 @@ -name: invariant-proxy-test-stack +name: invariant-gateway-test-stack services: traefik: image: traefik:v2.0 - container_name: "invariant-proxy-test-traefik" + container_name: "invariant-gateway-test-traefik" command: - --providers.docker=true # Enable the API handler in insecure mode, @@ -10,50 +10,50 @@ services: # on the entry point named traefik. - --api.insecure=true # Define Traefik entry points to port [80] for http and port [443] for https. - - --entrypoints.invariant-proxy-web-test.address=0.0.0.0:80 + - --entrypoints.invariant-gateway-web-test.address=0.0.0.0:80 networks: - - invariant-proxy-web-test + - invariant-gateway-web-test ports: - '${PORT_HTTP:-80}:80' volumes: - /var/run/docker.sock:/var/run/docker.sock labels: - "traefik.enable=true" - - "traefik.http.routers.traefik-http.entrypoints=invariant-proxy-web-test" + - "traefik.http.routers.traefik-http.entrypoints=invariant-gateway-web-test" - invariant-proxy: - container_name: invariant-proxy-test + invariant-gateway: + container_name: invariant-gateway-test build: - context: ../proxy - dockerfile: ../proxy/Dockerfile.proxy + context: ../gateway + dockerfile: ../gateway/Dockerfile.gateway depends_on: app-api: condition: service_healthy - working_dir: /srv/proxy + working_dir: /srv/gateway env_file: - .env.test environment: - DEV_MODE=true volumes: - type: bind - source: ../proxy - target: /srv/proxy + source: ../gateway + target: /srv/gateway networks: - - invariant-proxy-web-test + - invariant-gateway-web-test ports: [] labels: - "traefik.enable=true" - - "traefik.http.routers.invariant-proxy-api.rule=(Host(`localhost`) && PathPrefix(`/api/v1/proxy/`)) || (Host(`127.0.0.1`) && PathPrefix(`/api/v1/proxy/`))" - - "traefik.http.routers.invariant-proxy-api.entrypoints=invariant-proxy-web-test" - - "traefik.http.services.invariant-proxy-api.loadbalancer.server.port=8000" - - "traefik.docker.network=invariant-proxy-web-test" + - "traefik.http.routers.invariant-gateway-api.rule=(Host(`localhost`) && PathPrefix(`/api/v1/gateway/`)) || (Host(`127.0.0.1`) && PathPrefix(`/api/v1/gateway/`))" + - "traefik.http.routers.invariant-gateway-api.entrypoints=invariant-gateway-web-test" + - "traefik.http.services.invariant-gateway-api.loadbalancer.server.port=8000" + - "traefik.docker.network=invariant-gateway-web-test" healthcheck: - test: curl -X GET -I http://localhost:8000/api/v1/proxy/health --fail + test: curl -X GET -I http://localhost:8000/api/v1/gateway/health --fail interval: 1s timeout: 5s app-api: - container_name: invariant-proxy-test-explorer-app-api + container_name: invariant-gateway-test-explorer-app-api image: ghcr.io/invariantlabs-ai/explorer/app-api:latest platform: linux/amd64 depends_on: @@ -73,22 +73,22 @@ services: - PORT_API=80 networks: - internal - - invariant-proxy-web-test + - invariant-gateway-web-test volumes: - - /tmp/invariant-proxy-test/configs/explorer.test.yml:/config/explorer.config.yml + - /tmp/invariant-gateway-test/configs/explorer.test.yml:/config/explorer.config.yml labels: - "traefik.enable=true" - "traefik.http.routers.invariant-test-api.rule=(Host(`localhost`) && PathPrefix(`/api/`)) || (Host(`127.0.0.1`) && PathPrefix(`/api/`))" - - "traefik.http.routers.invariant-test-api.entrypoints=invariant-proxy-web-test" + - "traefik.http.routers.invariant-test-api.entrypoints=invariant-gateway-web-test" - "traefik.http.services.invariant-test-api.loadbalancer.server.port=8000" - - "traefik.docker.network=invariant-proxy-web-test" + - "traefik.docker.network=invariant-gateway-web-test" healthcheck: test: curl -X GET -I http://localhost:8000/api/v1/ --fail interval: 1s timeout: 5s database: - container_name: invariant-proxy-test-database + container_name: invariant-gateway-test-database image: postgres:16 env_file: - .env.test @@ -101,6 +101,6 @@ services: retries: 5 networks: - invariant-proxy-web-test: + invariant-gateway-web-test: external: true internal: diff --git a/tests/open_ai/test_chat_with_tool_call.py b/tests/open_ai/test_chat_with_tool_call.py index 12999f4..5afaa28 100644 --- a/tests/open_ai/test_chat_with_tool_call.py +++ b/tests/open_ai/test_chat_with_tool_call.py @@ -1,4 +1,4 @@ -"""Test the chat completions proxy calls with tool calling and processing response.""" +"""Test the chat completions gateway calls with tool calling and processing response.""" import json import os @@ -21,10 +21,10 @@ pytest_plugins = ("pytest_asyncio",) @pytest.mark.skipif(not os.getenv("OPENAI_API_KEY"), reason="No OPENAI_API_KEY set") @pytest.mark.parametrize("push_to_explorer", [False, True]) async def test_chat_completion_with_tool_call_without_streaming( - context, explorer_api_url, proxy_url, push_to_explorer + context, explorer_api_url, gateway_url, push_to_explorer ): """ - Test the chat completions proxy calls with tool calling and response processing + Test the chat completions gateway calls with tool calling and response processing without streaming. """ dataset_name = "test-dataset-open-ai-tool-call-" + str(uuid.uuid4()) @@ -35,9 +35,9 @@ async def test_chat_completion_with_tool_call_without_streaming( "Invariant-Authorization": "Bearer " }, # This key is not used for local tests ), - base_url=f"{proxy_url}/api/v1/proxy/{dataset_name}/openai" + base_url=f"{gateway_url}/api/v1/gateway/{dataset_name}/openai" if push_to_explorer - else f"{proxy_url}/api/v1/proxy/openai", + else f"{gateway_url}/api/v1/gateway/openai", ) chat_response = client.chat.completions.create( @@ -131,10 +131,10 @@ async def test_chat_completion_with_tool_call_without_streaming( @pytest.mark.skipif(not os.getenv("OPENAI_API_KEY"), reason="No OPENAI_API_KEY set") @pytest.mark.parametrize("push_to_explorer", [False, True]) async def test_chat_completion_with_tool_call_with_streaming( - context, explorer_api_url, proxy_url, push_to_explorer + context, explorer_api_url, gateway_url, push_to_explorer ): """ - Test the chat completions proxy calls with tool calling and response processing + Test the chat completions gateway calls with tool calling and response processing while streaming. """ dataset_name = "test-dataset-open-ai-tool-call-" + str(uuid.uuid4()) @@ -145,9 +145,9 @@ async def test_chat_completion_with_tool_call_with_streaming( "Invariant-Authorization": "Bearer " }, # This key is not used for local tests ), - base_url=f"{proxy_url}/api/v1/proxy/{dataset_name}/openai" + base_url=f"{gateway_url}/api/v1/gateway/{dataset_name}/openai" if push_to_explorer - else f"{proxy_url}/api/v1/proxy/openai", + else f"{gateway_url}/api/v1/gateway/openai", ) chat_response = client.chat.completions.create( diff --git a/tests/open_ai/test_chat_without_tool_calls.py b/tests/open_ai/test_chat_without_tool_calls.py index c3355d4..6d52e21 100644 --- a/tests/open_ai/test_chat_without_tool_calls.py +++ b/tests/open_ai/test_chat_without_tool_calls.py @@ -1,4 +1,4 @@ -"""Test the chat completions proxy calls without tool calling.""" +"""Test the chat completions gateway calls without tool calling.""" import base64 import os @@ -27,9 +27,9 @@ pytest_plugins = ("pytest_asyncio",) [(True, True), (True, False), (False, True), (False, False)], ) async def test_chat_completion( - context, explorer_api_url, proxy_url, do_stream, push_to_explorer + context, explorer_api_url, gateway_url, do_stream, push_to_explorer ): - """Test the chat completions proxy calls without tool calling.""" + """Test the chat completions gateway calls without tool calling.""" dataset_name = "test-dataset-open-ai-" + str(uuid.uuid4()) client = OpenAI( @@ -38,9 +38,9 @@ async def test_chat_completion( "Invariant-Authorization": "Bearer " }, # This key is not used for local tests ), - base_url=f"{proxy_url}/api/v1/proxy/{dataset_name}/openai" + base_url=f"{gateway_url}/api/v1/gateway/{dataset_name}/openai" if push_to_explorer - else f"{proxy_url}/api/v1/proxy/openai", + else f"{gateway_url}/api/v1/gateway/openai", ) chat_response = client.chat.completions.create( @@ -92,9 +92,9 @@ async def test_chat_completion( @pytest.mark.skipif(not os.getenv("OPENAI_API_KEY"), reason="No OPENAI_API_KEY set") @pytest.mark.parametrize("push_to_explorer", [True, False]) async def test_chat_completion_with_image( - context, explorer_api_url, proxy_url, push_to_explorer + context, explorer_api_url, gateway_url, push_to_explorer ): - """Test the chat completions proxy works with image.""" + """Test the chat completions gateway works with image.""" dataset_name = "test-dataset-open-ai-" + str(uuid.uuid4()) client = OpenAI( @@ -103,9 +103,9 @@ async def test_chat_completion_with_image( "Invariant-Authorization": "Bearer " }, # This key is not used for local tests ), - base_url=f"{proxy_url}/api/v1/proxy/{dataset_name}/openai" + base_url=f"{gateway_url}/api/v1/gateway/{dataset_name}/openai" if push_to_explorer - else f"{proxy_url}/api/v1/proxy/openai", + else f"{gateway_url}/api/v1/gateway/openai", ) image_path = Path(__file__).parent.parent / "images" / "two-cats.png" with image_path.open("rb") as image_file: @@ -176,9 +176,9 @@ async def test_chat_completion_with_image( @pytest.mark.skipif(not os.getenv("OPENAI_API_KEY"), reason="No OPENAI_API_KEY set") async def test_chat_completion_with_invariant_key_in_openai_key_header( - context, explorer_api_url, proxy_url + context, explorer_api_url, gateway_url ): - """Test the chat completions proxy calls with the Invariant API Key in the OpenAI Key header.""" + """Test the chat completions gateway calls with the Invariant API Key in the OpenAI Key header.""" dataset_name = "test-dataset-open-ai-" + str(uuid.uuid4()) openai_api_key = os.getenv("OPENAI_API_KEY") with patch.dict( @@ -187,7 +187,7 @@ async def test_chat_completion_with_invariant_key_in_openai_key_header( ): client = OpenAI( http_client=Client(), - base_url=f"{proxy_url}/api/v1/proxy/{dataset_name}/openai", + base_url=f"{gateway_url}/api/v1/gateway/{dataset_name}/openai", ) chat_response = client.chat.completions.create( @@ -229,8 +229,8 @@ async def test_chat_completion_with_invariant_key_in_openai_key_header( @pytest.mark.skip(reason="Skipping this test: OpenAI error scenario") @pytest.mark.parametrize("do_stream", [True, False]) -async def test_chat_completion_with_openai_exception(proxy_url, do_stream): - """Test the chat completions proxy call when OpenAI API fails.""" +async def test_chat_completion_with_openai_exception(gateway_url, do_stream): + """Test the chat completions gateway call when OpenAI API fails.""" client = OpenAI( http_client=Client( @@ -238,7 +238,7 @@ async def test_chat_completion_with_openai_exception(proxy_url, do_stream): "Invariant-Authorization": "Bearer " }, # This key is not used for local tests ), - base_url=f"{proxy_url}/api/v1/proxy/{"test-dataset-open-ai-" + str(uuid.uuid4())}/openai", + base_url=f"{gateway_url}/api/v1/gateway/{"test-dataset-open-ai-" + str(uuid.uuid4())}/openai", ) with pytest.raises(Exception) as exc_info: diff --git a/tests/util.py b/tests/util.py index 30826aa..5b6a6bc 100644 --- a/tests/util.py +++ b/tests/util.py @@ -7,10 +7,10 @@ from playwright.async_api import async_playwright @pytest.fixture -def proxy_url(): - if "INVARIANT_PROXY_API_URL" in os.environ: - return os.environ["INVARIANT_PROXY_API_URL"] - raise ValueError("Please set the INVARIANT_PROXY_API_URL environment variable") +def gateway_url(): + if "INVARIANT_GATEWAY_API_URL" in os.environ: + return os.environ["INVARIANT_GATEWAY_API_URL"] + raise ValueError("Please set the INVARIANT_GATEWAY_API_URL environment variable") @pytest.fixture