Rename proxy to gateway.

This commit is contained in:
Hemang
2025-03-05 14:35:39 +01:00
parent add900b302
commit 7008f73310
31 changed files with 196 additions and 197 deletions

2
.env
View File

@@ -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://<app-api-docker-container-name>:8000 to push to the local explorer instance.

View File

@@ -1,4 +1,4 @@
name: Invariant proxy testing CI
name: Invariant gateway testing CI
on:
push:

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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 = {

View File

@@ -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/<version>/models/<model-name>:generateContent or \
/api/v1/proxy/<dataset-name>/gemini/<version>models/<model-name>:generateContent",
/api/v1/gateway/gemini/<version>/models/<model-name>:generateContent or \
/api/v1/gateway/<dataset-name>/gemini/<version>models/<model-name>: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

View File

@@ -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

36
gateway/serve.py Normal file
View File

@@ -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)

View File

@@ -1,4 +1,4 @@
"""Common constants used in the proxy."""
"""Common constants used in the gateway."""
IGNORED_HEADERS = [
"accept-encoding",

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

22
run.sh
View File

@@ -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 $@
}
# -----------------------------

View File

@@ -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

View File

@@ -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",

View File

@@ -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"

View File

@@ -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"]

View File

@@ -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:

View File

@@ -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 <some-key>"
}, # 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 <some-key>"
}, # 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(

View File

@@ -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 <some-key>"
}, # 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 <some-key>"
}, # 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 <some-key>"
}, # 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:

View File

@@ -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