Loading Cursor config...
+
+ const configJson = JSON.stringify(data.config, null, 2)
+
+ return (
+
+
Cursor IDE Setup
+
+
+ - Open Cursor Settings (
Cmd+,)
+ - Go to Models > OpenAI API Compatible
+ - Paste the configuration below
+ - Click Validate Connection to verify
+
+
+
{configJson}
+
+
+ {data.notes.map((note, i) => (
+ - {note}
+ ))}
+
+
+
+
+
+
+
+ {validationError && (
+
Validation failed: {validationError}
+ )}
+
+ {validation && (
+
+ {validation.checks.map((check, i) => {
+ const icon = check.status === 'pass' ? '✓' : check.status === 'fail' ? '✗' : 'ℹ'
+ return (
+ -
+ {icon}
+ {check.name}
+ {check.detail && (
+ {check.detail}
+ )}
+
+ )
+ })}
+
+ )}
+
+ )
+}
diff --git a/better-agent-gateway/server/routes/__init__.py b/better-agent-gateway/server/routes/__init__.py
index 2d1aad8..7374eda 100644
--- a/better-agent-gateway/server/routes/__init__.py
+++ b/better-agent-gateway/server/routes/__init__.py
@@ -3,6 +3,7 @@
from .audit_routes import router as audit_router
from .auth import router as auth_router
from .chat import router as chat_router
+from .cursor_config import router as cursor_config_router
from .health import router as health_router
from .models import router as models_router
from .permissions import router as permissions_router
@@ -12,6 +13,7 @@
api_router.include_router(audit_router, tags=["audit"])
api_router.include_router(auth_router, tags=["auth"])
api_router.include_router(chat_router, tags=["chat"])
+api_router.include_router(cursor_config_router, tags=["config"])
api_router.include_router(health_router, tags=["health"])
api_router.include_router(models_router, tags=["models"])
api_router.include_router(permissions_router, tags=["permissions"])
diff --git a/better-agent-gateway/server/routes/cursor_config.py b/better-agent-gateway/server/routes/cursor_config.py
new file mode 100644
index 0000000..2a2f048
--- /dev/null
+++ b/better-agent-gateway/server/routes/cursor_config.py
@@ -0,0 +1,114 @@
+from fastapi import APIRouter, Request
+
+router = APIRouter()
+
+
+def _get_alias_registry():
+ from ..alias_registry import get_registry
+ return get_registry()
+
+
+def _get_gateway_base_url(request: Request) -> str:
+ """Derive the public-facing gateway URL.
+
+ Priority: DATABRICKS_APP_URL env var (set by the Databricks App runtime),
+ then X-Forwarded-Host proxy header, then the request's own base URL.
+ """
+ import os
+
+ app_url = os.getenv("DATABRICKS_APP_URL")
+ if app_url:
+ return app_url.rstrip("/")
+
+ forwarded_host = request.headers.get("x-forwarded-host")
+ forwarded_proto = request.headers.get("x-forwarded-proto", "https")
+ if forwarded_host:
+ return f"{forwarded_proto}://{forwarded_host}"
+
+ base = request.base_url
+ return f"{base.scheme}://{base.netloc}"
+
+
+@router.get("/v1/config/cursor")
+def get_cursor_config(request: Request):
+ registry = _get_alias_registry()
+ aliases = registry.list_aliases()
+ model_names = sorted(aliases.keys())
+
+ gateway_url = _get_gateway_base_url(request)
+
+ return {
+ "instructions": "Copy this configuration into Cursor Settings > Models > OpenAI API Compatible",
+ "config": {
+ "apiKey": "databricks-oauth",
+ "baseUrl": f"{gateway_url}/api/v1",
+ "models": model_names,
+ },
+ "notes": [
+ "Authentication: Visit the gateway URL in your browser first to establish a session, OR use a Databricks OAuth token as the API key",
+ "The apiKey value shown is a placeholder — Cursor requires a non-empty value but the gateway authenticates via your browser session or Bearer token",
+ "Models listed are auto-discovered aliases that resolve to the latest version of each model family",
+ ],
+ }
+
+
+@router.post("/v1/config/cursor/validate")
+def validate_cursor_config(request: Request):
+ checks = []
+
+ # Check 1: gateway health (inline — same process)
+ checks.append({
+ "name": "gateway_health",
+ "status": "pass",
+ "detail": "Gateway is healthy",
+ })
+
+ # Check 2: models available
+ registry = _get_alias_registry()
+ aliases = registry.list_aliases()
+ alias_count = len(aliases)
+ if alias_count > 0:
+ checks.append({
+ "name": "models_available",
+ "status": "pass",
+ "detail": f"Found {alias_count} model aliases",
+ })
+ else:
+ checks.append({
+ "name": "models_available",
+ "status": "fail",
+ "detail": "No model aliases found — registry may not be populated yet",
+ })
+
+ # Check 3: auth configured
+ from ..auth import extract_request_context
+ x_forwarded_access_token = request.headers.get("x-forwarded-access-token")
+ x_forwarded_user_id = request.headers.get("x-forwarded-user-id")
+ x_forwarded_email = request.headers.get("x-forwarded-email")
+ authorization = request.headers.get("authorization")
+
+ try:
+ ctx = extract_request_context(
+ x_forwarded_access_token=x_forwarded_access_token,
+ x_forwarded_user_id=x_forwarded_user_id,
+ x_forwarded_email=x_forwarded_email,
+ authorization=authorization,
+ )
+ checks.append({
+ "name": "auth_configured",
+ "status": "pass",
+ "detail": f"Authenticated as {ctx.user_id} via {ctx.auth_mode}",
+ })
+ except ValueError:
+ checks.append({
+ "name": "auth_configured",
+ "status": "info",
+ "detail": "No authentication detected — visit the gateway URL in your browser or provide a Bearer token",
+ })
+
+ overall_valid = all(c["status"] in ("pass", "info") for c in checks)
+
+ return {
+ "valid": overall_valid,
+ "checks": checks,
+ }
diff --git a/better-agent-gateway/server/routes/models.py b/better-agent-gateway/server/routes/models.py
index 5a2024f..be586cc 100644
--- a/better-agent-gateway/server/routes/models.py
+++ b/better-agent-gateway/server/routes/models.py
@@ -1,4 +1,4 @@
-from fastapi import APIRouter
+from fastapi import APIRouter, Request
router = APIRouter()
@@ -11,7 +11,61 @@ def _get_alias_registry():
@router.get("/v1/models")
def list_models():
registry = _get_alias_registry()
+ aliases = registry.list_aliases()
+ endpoints = registry.list_endpoints()
+
+ # Build OpenAI-standard model objects for all endpoints and aliases
+ seen: set[str] = set()
+ data: list[dict] = []
+
+ for alias_name in sorted(aliases.keys()):
+ if alias_name not in seen:
+ seen.add(alias_name)
+ data.append({
+ "id": alias_name,
+ "object": "model",
+ "created": 0,
+ "owned_by": "databricks",
+ })
+
+ for ep_name in endpoints:
+ if ep_name not in seen:
+ seen.add(ep_name)
+ data.append({
+ "id": ep_name,
+ "object": "model",
+ "created": 0,
+ "owned_by": "databricks",
+ })
+
+ return {
+ "object": "list",
+ "data": data,
+ "aliases": aliases,
+ "endpoints": endpoints,
+ }
+
+
+@router.get("/v1/config/cursor")
+def cursor_config(request: Request):
+ """Return a Cursor-compatible configuration snippet.
+
+ Lists all available model IDs (aliases + endpoints) so Cursor
+ users can configure the gateway as an OpenAI-compatible provider.
+ """
+ registry = _get_alias_registry()
+ aliases = registry.list_aliases()
+ endpoints = registry.list_endpoints()
+
+ # Collect all usable model names: aliases first, then endpoints
+ models: list[str] = sorted(aliases.keys()) + [
+ ep for ep in endpoints if ep not in aliases.values()
+ ]
+
+ base_url = str(request.base_url).rstrip("/") + "/api/v1"
+
return {
- "aliases": registry.list_aliases(),
- "endpoints": registry.list_endpoints(),
+ "models": models,
+ "base_url": base_url,
+ "provider": "databricks",
}
diff --git a/better-agent-gateway/tests/test_cursor_config.py b/better-agent-gateway/tests/test_cursor_config.py
new file mode 100644
index 0000000..7b1f70c
--- /dev/null
+++ b/better-agent-gateway/tests/test_cursor_config.py
@@ -0,0 +1,117 @@
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).parent.parent))
+
+from fastapi.testclient import TestClient
+
+from app import app
+
+client = TestClient(app)
+
+
+def test_get_cursor_config_returns_expected_structure():
+ response = client.get("/api/v1/config/cursor")
+ assert response.status_code == 200
+ data = response.json()
+ assert "instructions" in data
+ assert "config" in data
+ assert "notes" in data
+
+ config = data["config"]
+ assert "apiKey" in config
+ assert "baseUrl" in config
+ assert "models" in config
+ assert isinstance(config["models"], list)
+
+
+def test_get_cursor_config_base_url_uses_request_host():
+ response = client.get("/api/v1/config/cursor")
+ assert response.status_code == 200
+ base_url = response.json()["config"]["baseUrl"]
+ assert base_url.endswith("/api/v1")
+ assert base_url.startswith("http")
+
+
+def test_get_cursor_config_base_url_respects_forwarded_headers():
+ response = client.get(
+ "/api/v1/config/cursor",
+ headers={
+ "x-forwarded-host": "my-gateway.aws.databricksapps.com",
+ "x-forwarded-proto": "https",
+ },
+ )
+ assert response.status_code == 200
+ base_url = response.json()["config"]["baseUrl"]
+ assert base_url == "https://my-gateway.aws.databricksapps.com/api/v1"
+
+
+def test_get_cursor_config_base_url_prefers_databricks_app_url(monkeypatch):
+ monkeypatch.setenv("DATABRICKS_APP_URL", "https://my-app.aws.databricksapps.com")
+ response = client.get("/api/v1/config/cursor")
+ assert response.status_code == 200
+ base_url = response.json()["config"]["baseUrl"]
+ assert base_url == "https://my-app.aws.databricksapps.com/api/v1"
+
+
+def test_get_cursor_config_notes_is_list():
+ response = client.get("/api/v1/config/cursor")
+ assert response.status_code == 200
+ notes = response.json()["notes"]
+ assert isinstance(notes, list)
+ assert len(notes) > 0
+
+
+def test_validate_cursor_config_returns_valid_structure():
+ response = client.post("/api/v1/config/cursor/validate")
+ assert response.status_code == 200
+ data = response.json()
+ assert "valid" in data
+ assert "checks" in data
+ assert isinstance(data["valid"], bool)
+ assert isinstance(data["checks"], list)
+
+
+def test_validate_cursor_config_has_required_checks():
+ response = client.post("/api/v1/config/cursor/validate")
+ assert response.status_code == 200
+ checks = response.json()["checks"]
+ check_names = {c["name"] for c in checks}
+ assert "gateway_health" in check_names
+ assert "models_available" in check_names
+ assert "auth_configured" in check_names
+
+
+def test_validate_cursor_config_check_structure():
+ response = client.post("/api/v1/config/cursor/validate")
+ assert response.status_code == 200
+ for check in response.json()["checks"]:
+ assert "name" in check
+ assert "status" in check
+ assert "detail" in check
+ assert check["status"] in ("pass", "fail", "info")
+
+
+def test_validate_cursor_config_gateway_health_passes():
+ response = client.post("/api/v1/config/cursor/validate")
+ assert response.status_code == 200
+ checks = {c["name"]: c for c in response.json()["checks"]}
+ assert checks["gateway_health"]["status"] == "pass"
+
+
+def test_validate_cursor_config_auth_info_without_token():
+ response = client.post("/api/v1/config/cursor/validate")
+ assert response.status_code == 200
+ checks = {c["name"]: c for c in response.json()["checks"]}
+ # Without auth headers, auth_configured should be "info" (not a hard failure)
+ assert checks["auth_configured"]["status"] in ("pass", "info")
+
+
+def test_validate_cursor_config_auth_pass_with_bearer_token():
+ response = client.post(
+ "/api/v1/config/cursor/validate",
+ headers={"Authorization": "Bearer fake-test-token"},
+ )
+ assert response.status_code == 200
+ checks = {c["name"]: c for c in response.json()["checks"]}
+ assert checks["auth_configured"]["status"] == "pass"
diff --git a/better-agent-gateway/tests/test_openai_compat.py b/better-agent-gateway/tests/test_openai_compat.py
new file mode 100644
index 0000000..9e9f29c
--- /dev/null
+++ b/better-agent-gateway/tests/test_openai_compat.py
@@ -0,0 +1,134 @@
+"""Tests for OpenAI-compatible /v1/models response format.
+
+Cursor and the OpenAI SDK expect the standard OpenAI list format:
+ {"object": "list", "data": [{"id": "...", "object": "model", ...}]}
+
+These tests verify our /v1/models endpoint returns this format while
+also preserving backward-compatible aliases/endpoints metadata.
+"""
+from fastapi.testclient import TestClient
+
+from app import app
+
+client = TestClient(app)
+
+
+def test_models_response_has_openai_list_format():
+ """GET /api/v1/models returns top-level 'object' and 'data' keys."""
+ response = client.get("/api/v1/models")
+ assert response.status_code == 200
+ body = response.json()
+ assert body["object"] == "list", "top-level 'object' must be 'list'"
+ assert isinstance(body["data"], list), "'data' must be a list"
+
+
+def test_models_data_items_have_openai_model_schema():
+ """Each item in 'data' must have id, object, created, owned_by."""
+ # Seed the registry so there's at least one model
+ from server.alias_registry import get_registry
+ registry = get_registry()
+ registry.refresh_from_endpoints([
+ {"name": "databricks-claude-sonnet-4-6", "state": {"ready": "READY"}},
+ ])
+
+ response = client.get("/api/v1/models")
+ body = response.json()
+ assert len(body["data"]) > 0, "data should contain at least one model"
+
+ for model in body["data"]:
+ assert "id" in model, "each model must have 'id'"
+ assert model["object"] == "model", "each model object must be 'model'"
+ assert "created" in model, "each model must have 'created'"
+ assert "owned_by" in model, "each model must have 'owned_by'"
+
+
+def test_models_includes_aliases_and_endpoints():
+ """Both aliases (as model entries) and raw endpoints appear in data."""
+ from server.alias_registry import get_registry
+ registry = get_registry()
+ registry.refresh_from_endpoints([
+ {"name": "databricks-claude-sonnet-4-6", "state": {"ready": "READY"}},
+ ])
+
+ response = client.get("/api/v1/models")
+ body = response.json()
+ model_ids = {m["id"] for m in body["data"]}
+ # The endpoint itself should appear
+ assert "databricks-claude-sonnet-4-6" in model_ids
+ # The alias (claude-sonnet-latest) should also appear
+ assert "claude-sonnet-latest" in model_ids
+
+
+def test_models_preserves_backward_compat_keys():
+ """Response still includes 'aliases' and 'endpoints' for the frontend."""
+ from server.alias_registry import get_registry
+ registry = get_registry()
+ registry.refresh_from_endpoints([
+ {"name": "databricks-claude-sonnet-4-6", "state": {"ready": "READY"}},
+ ])
+
+ response = client.get("/api/v1/models")
+ body = response.json()
+ assert "aliases" in body, "backward-compat 'aliases' key must be present"
+ assert "endpoints" in body, "backward-compat 'endpoints' key must be present"
+ assert body["aliases"]["claude-sonnet-latest"] == "databricks-claude-sonnet-4-6"
+ assert "databricks-claude-sonnet-4-6" in body["endpoints"]
+
+
+def test_openai_sdk_models_response_format():
+ """Validate the response can be parsed as an OpenAI models list response.
+
+ The OpenAI SDK expects: {"object": "list", "data": [...]} where each
+ data item has at minimum: id, object, created, owned_by.
+ """
+ from server.alias_registry import get_registry
+ registry = get_registry()
+ registry.refresh_from_endpoints([
+ {"name": "databricks-claude-sonnet-4-6", "state": {"ready": "READY"}},
+ {"name": "databricks-gpt-4o", "state": {"ready": "READY"}},
+ ])
+
+ response = client.get("/api/v1/models")
+ body = response.json()
+
+ # Validate full OpenAI schema
+ assert body["object"] == "list"
+ assert isinstance(body["data"], list)
+ assert len(body["data"]) >= 2 # at least the 2 endpoints
+
+ for item in body["data"]:
+ assert isinstance(item["id"], str)
+ assert item["object"] == "model"
+ assert isinstance(item["created"], int)
+ assert isinstance(item["owned_by"], str)
+
+
+def test_cursor_config_models_match_openai_models():
+ """Fetch cursor config, build an OpenAI-style client config, and verify
+ the models listed in cursor config appear in /v1/models."""
+ from server.alias_registry import get_registry
+ registry = get_registry()
+ registry.refresh_from_endpoints([
+ {"name": "databricks-claude-sonnet-4-6", "state": {"ready": "READY"}},
+ ])
+
+ # Get cursor config
+ config_resp = client.get("/api/v1/config/cursor")
+ assert config_resp.status_code == 200, "cursor config endpoint must exist"
+ config = config_resp.json()
+
+ # Cursor config should list models (nested under config.models)
+ assert "config" in config, "cursor config must include 'config'"
+ assert "models" in config["config"], "cursor config.config must include 'models'"
+ cursor_models = config["config"]["models"]
+ assert len(cursor_models) > 0, "cursor config must list at least one model"
+
+ # Now verify those models appear in /v1/models
+ models_resp = client.get("/api/v1/models")
+ models_body = models_resp.json()
+ available_ids = {m["id"] for m in models_body["data"]}
+
+ for model_name in cursor_models:
+ assert model_name in available_ids, (
+ f"cursor config model '{model_name}' must appear in /v1/models data"
+ )