diff --git a/.gitignore b/.gitignore index 4f8924d..b9f9582 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist/ .uv/ *.egg-info/ .env +.vscode/ diff --git a/AGENTS.md b/AGENTS.md index aaacb69..b32a45f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,7 @@ This project is designed for AI-first development. All agents MUST follow these - **PROCESS**: 1. If a task involves a new technology/library: **STOP**. 2. Perform a `web_search` or `librarian` task to investigate best practices, common pitfalls, and API usage. - 3. Document findings in `docs/skills/.md`. + 3. Document findings in `.agents/skills/.md`. 4. Only proceed with implementation once the skill file exists and is reviewed. ### 2. Architecture First diff --git a/config.yaml b/config.yaml index ae6733e..dfc7695 100644 --- a/config.yaml +++ b/config.yaml @@ -4,6 +4,11 @@ # API key for authentication (optional - if not set, no auth required) api-key: "your-secret-api-key" +# CORS configuration +cors: + allow-origins: + - "http://localhost:8000" + # Models configuration (used by models router) models: - id: "gpt-4o" diff --git a/src/llmock/app.py b/src/llmock/app.py index 007a8ba..5a7fa52 100644 --- a/src/llmock/app.py +++ b/src/llmock/app.py @@ -1,8 +1,7 @@ """FastAPI application factory and setup.""" -from collections.abc import Callable - from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from starlette.middleware.base import BaseHTTPMiddleware @@ -13,19 +12,22 @@ class APIKeyMiddleware(BaseHTTPMiddleware): """Middleware to validate API key for all requests except health.""" - def __init__(self, app, config_getter: Callable[[], Config] = get_config): - """Initialize middleware with config getter.""" + def __init__(self, app, config: Config): + """Initialize middleware with config.""" super().__init__(app) - self.config_getter = config_getter + self.config = config async def dispatch(self, request: Request, call_next): """Check API key before processing request.""" + # Skip auth for OPTIONS preflight requests (CORS) + if request.method == "OPTIONS": + return await call_next(request) + # Skip auth for health endpoint if request.url.path == "/health": return await call_next(request) - config = self.config_getter() - config_api_key = config.get("api-key") + config_api_key = self.config.get("api-key") # If no API key configured, allow all requests if not config_api_key: @@ -49,12 +51,25 @@ async def dispatch(self, request: Request, call_next): return await call_next(request) -def create_app(config_getter: Callable[[], Config] = get_config) -> FastAPI: +def create_app(config: Config = get_config()) -> FastAPI: """Create and configure the FastAPI application.""" app = FastAPI(title="llmock") + # Get CORS origins from config + cors_config = config.get("cors", {}) + allow_origins = cors_config.get("allow-origins", ["http://localhost:8000"]) + + # Add CORS middleware to allow browser connections + app.add_middleware( + CORSMiddleware, + allow_origins=allow_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + # Add API key middleware - app.add_middleware(APIKeyMiddleware, config_getter=config_getter) + app.add_middleware(APIKeyMiddleware, config=config) # Include routers app.include_router(health.router) diff --git a/tests/conftest.py b/tests/conftest.py index cec8253..3d5b283 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,7 +24,7 @@ def test_config() -> Config: @pytest.fixture async def client(test_config: Config) -> AsyncGenerator[AsyncClient, None]: """Provide an async HTTP client for testing.""" - app = create_app(config_getter=lambda: test_config) + app = create_app(config=test_config) # Override the config dependency for testing app.dependency_overrides[get_config] = lambda: test_config diff --git a/tests/test_api_key.py b/tests/test_api_key.py index 8b08de1..3af35b7 100644 --- a/tests/test_api_key.py +++ b/tests/test_api_key.py @@ -28,7 +28,7 @@ async def client_with_auth( config_with_api_key: Config, ) -> AsyncGenerator[AsyncClient, None]: """Client for app with API key configured.""" - app = create_app(config_getter=lambda: config_with_api_key) + app = create_app(config=config_with_api_key) app.dependency_overrides[get_config] = lambda: config_with_api_key transport = ASGITransport(app=app) @@ -41,7 +41,7 @@ async def client_no_auth( config_without_api_key: Config, ) -> AsyncGenerator[AsyncClient, None]: """Client for app without API key configured.""" - app = create_app(config_getter=lambda: config_without_api_key) + app = create_app(config=config_without_api_key) app.dependency_overrides[get_config] = lambda: config_without_api_key transport = ASGITransport(app=app) diff --git a/tests/test_chat.py b/tests/test_chat.py index b9b5b70..fcab0ea 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -27,7 +27,7 @@ def test_config() -> Config: @pytest.fixture async def openai_client(test_config: Config) -> AsyncGenerator[AsyncOpenAI, None]: """Provide an AsyncOpenAI client using ASGI transport (no real server needed).""" - app = create_app(config_getter=lambda: test_config) + app = create_app(config=test_config) app.dependency_overrides[get_config] = lambda: test_config # Use httpx with ASGITransport to test without spinning up a real server diff --git a/tests/test_models.py b/tests/test_models.py index b368e53..bb946e2 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -28,7 +28,7 @@ def test_config() -> Config: @pytest.fixture async def openai_client(test_config: Config) -> AsyncGenerator[AsyncOpenAI, None]: """Provide an AsyncOpenAI client using ASGI transport (no real server needed).""" - app = create_app(config_getter=lambda: test_config) + app = create_app(config=test_config) app.dependency_overrides[get_config] = lambda: test_config # Use httpx with ASGITransport to test without spinning up a real server diff --git a/tests/test_responses.py b/tests/test_responses.py index 7a06f0d..7f7d90a 100644 --- a/tests/test_responses.py +++ b/tests/test_responses.py @@ -26,7 +26,7 @@ def test_config() -> Config: @pytest.fixture async def client(test_config: Config) -> AsyncGenerator[httpx.AsyncClient, None]: """Provide an async HTTP client for testing.""" - app = create_app(config_getter=lambda: test_config) + app = create_app(config=test_config) app.dependency_overrides[get_config] = lambda: test_config transport = httpx.ASGITransport(app=app)