Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion docs/commands/serve.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Starts an HTTP server for local testing or production deployment. Discovers DSPy
| `--python` | auto-detect | Path to Python interpreter |
| `--system` | disabled | Use system Python instead of venv |
| `--mcp` | disabled | Enable MCP server at `/mcp` |
| `--auth` | disabled | Require API authentication via `DSPY_API_KEY` |

## Auto-Discovery

Expand All @@ -56,6 +57,8 @@ Every deployment provides:
| `/<ModuleName>` | POST | Execute module with JSON request body |
| `/programs` | GET | List all discovered modules and their schemas |
| `/openapi.json` | GET | OpenAPI specification |
| `/login` | GET/POST | Browser login (when `--auth` enabled) |
| `/health` | GET | Health check (always open, even with `--auth`) |

With `--mcp` enabled:

Expand Down Expand Up @@ -98,11 +101,20 @@ Response:
### Production Mode

```bash
dspy-cli serve --no-reload --host 0.0.0.0 --port 8000
dspy-cli serve --no-reload --host 0.0.0.0 --port 8000 --auth
```

Disables hot reload and binds to all network interfaces for production deployment.

With `--auth` enabled:

- Set `DSPY_API_KEY` in the environment.
- API clients must send `Authorization: Bearer <DSPY_API_KEY>`.
- The browser UI uses `/login` with the same key.
- `/health` remains open for health checks.

If `DSPY_API_KEY` is not set, a temporary key is generated and logged at startup.

## Error Responses

**Validation error (422):**
Expand Down
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ In `.env`:
```bash
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
DSPY_API_KEY=your-api-key # Optional: HTTP auth when running `dspy-cli serve --auth`
```

Reference in config:
Expand Down
24 changes: 19 additions & 5 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ flyctl auth login

# Launch and configure
flyctl launch
flyctl secrets set OPENAI_API_KEY=sk-proj-...
flyctl secrets set OPENAI_API_KEY=sk-proj-... DSPY_API_KEY=your-api-key
flyctl deploy
```

Expand All @@ -38,7 +38,9 @@ Verify:

```bash
curl -X POST "https://your-app.fly.dev/SummarizerPredict" \
-d '{"blog_post": "Test"}' -H "Content-Type: application/json"
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-api-key" \
-d '{"blog_post": "Test"}'
```

## Docker Deployment
Expand All @@ -49,9 +51,14 @@ Compatible with any platform that can serve arbitrary dockerfiles.

```bash
docker build -t my-app:latest .
docker run -p 8000:8000 -e OPENAI_API_KEY=sk-... my-app:latest
docker run -p 8000:8000 \
-e OPENAI_API_KEY=sk-... \
-e DSPY_API_KEY=your-api-key \
my-app:latest
```

> The generated Dockerfile runs `dspy-cli serve --auth` by default, so `DSPY_API_KEY` is required.

### Push to Registry

**Docker Hub:**
Expand Down Expand Up @@ -89,7 +96,7 @@ gcloud run deploy my-app \
--source . \
--region us-central1 \
--allow-unauthenticated \
--set-env-vars OPENAI_API_KEY=sk-proj-...
--set-env-vars OPENAI_API_KEY=sk-proj-...,DSPY_API_KEY=your-api-key
```

## Environment Variables
Expand All @@ -104,6 +111,7 @@ Use `.env` file (auto-loaded by `dspy-cli serve`, excluded via `.gitignore`):
# .env - Do not commit
OPENAI_API_KEY=sk-proj-...
ANTHROPIC_API_KEY=sk-ant-...
DSPY_API_KEY=your-api-key # Optional: required when running with --auth
```

### Production Secrets
Expand All @@ -116,6 +124,10 @@ Configure via platform secret managers:
| **Google Cloud Run** | `--set-env-vars KEY=value` or Secret Manager |
| **AWS** | Systems Manager Parameter Store / Secrets Manager |

When you first create a project, if you do not have a `DSPY_API_KEY`, one will be generated and you can see it in the logs.

It is recommended to change this key immediately after deployment to a secure value via your secret manager.

### Secrets Best Practices

**Required:**
Expand All @@ -137,9 +149,11 @@ Configure via platform secret managers:
Verify service availability:

```bash
curl https://my-app.fly.dev/openapi.json
curl https://my-app.fly.dev/health
```

When authentication is enabled, use `/health` (always open) or include `Authorization: Bearer <DSPY_API_KEY>` for other endpoints.

## Logs

**Fly.io:**
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ Or use interactive UI at `http://localhost:8000`.

## 5. Deploy

See [Deployment Guide](deployment.md) for Fly.io, Docker, AWS, GCP, and more.
See [Deployment Guide](deployment.md) for Fly.io, Docker, AWS, GCP, and more. Production Docker deployments enable authentication by default; set `DSPY_API_KEY` in your environment and use `Authorization: Bearer <key>` when calling your API.

## Development

Expand Down
7 changes: 7 additions & 0 deletions examples/blog-tools/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.git
.env
__pycache__
*.pyc
.vscode
.mypy_cache
logs/
4 changes: 1 addition & 3 deletions examples/blog-tools/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
COPY . .
RUN uv sync --no-dev

RUN uv tool install dspy-cli

EXPOSE 8000

CMD ["uv", "run", "dspy-cli", "serve", "--host", "0.0.0.0", "--port", "8000", "--auth"]
CMD ["uv", "run", "dspy-cli", "serve", "--host", "0.0.0.0", "--port", "8000", "--auth", "--no-reload"]
129 changes: 0 additions & 129 deletions examples/blog-tools/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -598,139 +598,10 @@
}
}
}
},
"/login": {
"get": {
"summary": "Login Form",
"description": "Render the login form.",
"operationId": "login_form_login_get",
"parameters": [
{
"name": "error",
"in": "query",
"required": false,
"schema": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Error"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"text/html": {
"schema": {
"type": "string"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"post": {
"summary": "Login",
"description": "Process login form submission.",
"operationId": "login_login_post",
"requestBody": {
"required": true,
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/Body_login_login_post"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/logout": {
"post": {
"summary": "Logout",
"description": "Log out by clearing the session cookie.",
"operationId": "logout_logout_post",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/health": {
"get": {
"summary": "Health",
"description": "Health check endpoint (always open).",
"operationId": "health_health_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Body_login_login_post": {
"properties": {
"token_input": {
"type": "string",
"title": "Token Input"
}
},
"type": "object",
"required": [
"token_input"
],
"title": "Body_login_login_post"
},
"HTTPValidationError": {
"properties": {
"detail": {
Expand Down
7 changes: 7 additions & 0 deletions examples/code-review-agent/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.git
.env
__pycache__
*.pyc
.vscode
.mypy_cache
logs/
4 changes: 1 addition & 3 deletions examples/code-review-agent/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
COPY . .
RUN uv sync --no-dev

RUN uv tool install dspy-cli

EXPOSE 8000

CMD ["uv", "run", "dspy-cli", "serve", "--host", "0.0.0.0", "--port", "8000", "--auth"]
CMD ["uv", "run", "dspy-cli", "serve", "--host", "0.0.0.0", "--port", "8000", "--auth", "--no-reload"]
2 changes: 1 addition & 1 deletion src/dspy_cli/commands/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def _exec_clean(target_python: Path, args: list[str]) -> NoReturn:
@click.option(
"--auth/--no-auth",
default=False,
help="Enable API authentication via DSPY_API_TOKEN (default: disabled)",
help="Enable API authentication via DSPY_API_KEY (default: disabled)",
)
def serve(port, host, logs_dir, reload, save_openapi, openapi_format, python, system, mcp, auth):
"""Start an HTTP API server that exposes your DSPy programs.
Expand Down
10 changes: 5 additions & 5 deletions src/dspy_cli/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def create_app(
package_name: Python package name for modules
logs_dir: Directory for log files
enable_ui: Whether to enable the web UI (always True, kept for compatibility)
enable_auth: Whether to enable API authentication via DSPY_API_TOKEN
enable_auth: Whether to enable API authentication via DSPY_API_KEY

Returns:
Configured FastAPI application
Expand Down Expand Up @@ -187,7 +187,7 @@ async def program_metrics(program_name: str):
logger.warning(f"Static directory not found: {static_dir}")

# Create UI routes
create_ui_routes(app, modules, config, logs_dir)
create_ui_routes(app, modules, config, logs_dir, auth_enabled=enable_auth)
logger.info("UI routes registered")

# Setup authentication if enabled
Expand All @@ -204,11 +204,11 @@ async def program_metrics(program_name: str):
# Auto-generate a token and log it (Jupyter-style)
token = generate_token()
import os as os_module
os_module.environ["DSPY_API_TOKEN"] = token
os_module.environ["DSPY_API_KEY"] = token
logger.warning("=" * 60)
logger.warning("DSPY_API_TOKEN not set. Generated temporary token:")
logger.warning("DSPY_API_KEY not set. Generated temporary token:")
logger.warning(f" {token}")
logger.warning("Set DSPY_API_TOKEN as an environment secret for a persistent token.")
logger.warning("Set DSPY_API_KEY as an environment secret for a persistent token.")
logger.warning("=" * 60)

# Add auth routes (login/logout)
Expand Down
8 changes: 4 additions & 4 deletions src/dspy_cli/server/auth.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Authentication module for DSPy API server.

Provides lightweight bearer token + session cookie authentication.
- API clients use: Authorization: Bearer <DSPY_API_TOKEN>
- API clients use: Authorization: Bearer <DSPY_API_KEY>
- Browser UI uses: Login form -> signed HttpOnly session cookie
"""

Expand All @@ -23,7 +23,7 @@
SESSION_MAX_AGE = 7 * 24 * 3600 # 7 days

# Environment variable names
ENV_API_TOKEN = "DSPY_API_TOKEN"
ENV_API_TOKEN = "DSPY_API_KEY"
ENV_AUTH_ENABLED = "DSPY_CLI_AUTH_ENABLED"

# Paths that don't require authentication
Expand Down Expand Up @@ -244,11 +244,11 @@ async def login_form(request: Request, error: str | None = None):
{error_html}
<form method="post">
<label for="token">API Token</label>
<input type="password" id="token" name="token" placeholder="Enter DSPY_API_TOKEN" autofocus required>
<input type="password" id="token" name="token" placeholder="Enter DSPY_API_KEY" autofocus required>
<button type="submit">Login</button>
</form>
<div class="help">
<p>The token is set via the <code>DSPY_API_TOKEN</code> environment variable.</p>
<p>The token is set via the <code>DSPY_API_KEY</code> environment variable.</p>
</div>
</div>
</body>
Expand Down
2 changes: 1 addition & 1 deletion src/dspy_cli/server/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ def notify_cli(msg: str, level: str = "info"):
click.echo(f" • {Path.cwd() / 'dspy.config.yaml'}")
click.echo()
if auth:
token = os.environ.get("DSPY_API_TOKEN")
token = os.environ.get("DSPY_API_KEY")
if token:
click.echo(click.style("Authentication: ENABLED", fg="green"))
click.echo(" API clients: Authorization: Bearer <token>")
Expand Down
Loading