mirror of
https://github.com/invariantlabs-ai/invariant-gateway.git
synced 2026-02-12 14:32:45 +00:00
Rename proxy to gateway.
This commit is contained in:
2
.env
2
.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://<app-api-docker-container-name>:8000 to push to the local explorer instance.
|
||||
|
||||
2
.github/workflows/tests_ci.yml
vendored
2
.github/workflows/tests_ci.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Invariant proxy testing CI
|
||||
name: Invariant gateway testing CI
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
@@ -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:
|
||||
|
||||
12
gateway/Dockerfile.gateway
Normal file
12
gateway/Dockerfile.gateway
Normal 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
|
||||
@@ -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
|
||||
@@ -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 = {
|
||||
@@ -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
|
||||
@@ -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
36
gateway/serve.py
Normal 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)
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Common constants used in the proxy."""
|
||||
"""Common constants used in the gateway."""
|
||||
|
||||
IGNORED_HEADERS = [
|
||||
"accept-encoding",
|
||||
13
gateway/validate_config.py
Normal file
13
gateway/validate_config.py
Normal 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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
22
run.sh
22
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 $@
|
||||
}
|
||||
|
||||
# -----------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user