Skip to content

Commit efcaac0

Browse files
committed
Add OpenAPI specification generation to serve command
Automatically generates OpenAPI 3.1.0 specs when the server starts. Features: - Generate OpenAPI spec on server start (enabled by default) - Support JSON and YAML formats via --openapi-format flag - Add --save-openapi/--no-save-openapi flag to control file generation - Enhance metadata with app_id and description from dspy.config.yaml - Add DSPy-specific extensions (program info, model mappings) - Spec always available at /openapi.json endpoint Implementation: - New utility module: src/dspy_cli/utils/openapi.py - Enhanced app.py to customize OpenAPI metadata - Updated runner.py to save spec file on startup - Added CLI flags to serve.py command The generated spec includes all discovered DSPy programs with their request/response schemas, plus custom x-dspy-* extensions containing program and model information.
1 parent dd08ecc commit efcaac0

File tree

5 files changed

+486
-5
lines changed

5 files changed

+486
-5
lines changed

docs/OPENAPI.md

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
# OpenAPI Specification Generation
2+
3+
The `dspy-cli serve` command automatically generates OpenAPI 3.1.0 specifications for your DSPy programs.
4+
5+
## Features
6+
7+
- **Automatic generation** on server start (enabled by default)
8+
- **Multiple formats** supported: JSON and YAML
9+
- **Enhanced metadata** from `dspy.config.yaml` (app_id, description)
10+
- **DSPy-specific extensions** with program and model information
11+
- **Always available** via `/openapi.json` endpoint, regardless of file generation
12+
13+
## Usage
14+
15+
### Default Behavior (JSON)
16+
17+
```bash
18+
dspy-cli serve
19+
# Creates openapi.json in project root
20+
```
21+
22+
### YAML Format
23+
24+
```bash
25+
dspy-cli serve --openapi-format yaml
26+
# Creates openapi.yaml in project root
27+
```
28+
29+
### Disable File Generation
30+
31+
```bash
32+
dspy-cli serve --no-save-openapi
33+
# Spec still available at http://localhost:8000/openapi.json
34+
```
35+
36+
## Generated Specification
37+
38+
### Standard OpenAPI Fields
39+
40+
The generated spec includes:
41+
42+
- **Endpoints**: All discovered DSPy programs as POST endpoints
43+
- **Request schemas**: Dynamically generated from DSPy module forward() signatures
44+
- **Response schemas**: Output types from your modules
45+
- **Validation**: Input/output validation via Pydantic models
46+
- **Additional endpoints**: `/programs` for listing all programs
47+
48+
### DSPy-Specific Extensions
49+
50+
The spec includes custom OpenAPI extensions (x-* fields):
51+
52+
```json
53+
{
54+
"info": {
55+
"title": "my-app-id",
56+
"description": "Description from dspy.config.yaml",
57+
"x-dspy-config": {
58+
"default_model": "openai:gpt-4",
59+
"programs_count": 3
60+
},
61+
"x-dspy-programs": [
62+
{
63+
"name": "CategorizerPredict",
64+
"module_path": "my_app.modules.categorizer_predict",
65+
"is_forward_typed": true
66+
}
67+
],
68+
"x-dspy-program-models": {
69+
"CategorizerPredict": "openai:gpt-4",
70+
"SummarizerCoT": "anthropic:claude-3-sonnet"
71+
}
72+
}
73+
}
74+
```
75+
76+
## Configuration
77+
78+
### Metadata Source
79+
80+
The OpenAPI spec pulls metadata from `dspy.config.yaml`:
81+
82+
```yaml
83+
app_id: my-blog-tools
84+
description: A set of functions for a content management system.
85+
```
86+
87+
These values populate the OpenAPI `title` and `description` fields.
88+
89+
### Fallback Defaults
90+
91+
If not specified in config:
92+
- **title**: "DSPy API"
93+
- **description**: "Automatically generated API for DSPy programs"
94+
- **version**: "0.1.0"
95+
96+
## Accessing the Specification
97+
98+
### As a File
99+
100+
```bash
101+
# After running dspy-cli serve
102+
cat openapi.json
103+
```
104+
105+
### Via HTTP Endpoint
106+
107+
```bash
108+
# While server is running
109+
curl http://localhost:8000/openapi.json
110+
```
111+
112+
### In Code
113+
114+
```python
115+
from fastapi import FastAPI
116+
from dspy_cli.utils.openapi import generate_openapi_spec
117+
118+
# After creating your FastAPI app
119+
spec = generate_openapi_spec(app)
120+
```
121+
122+
## Integration with Tools
123+
124+
### Swagger UI
125+
126+
FastAPI automatically provides interactive API documentation at:
127+
- **Swagger UI**: `http://localhost:8000/docs`
128+
- **ReDoc**: `http://localhost:8000/redoc`
129+
130+
### API Clients
131+
132+
Use the generated spec to create type-safe API clients:
133+
134+
```bash
135+
# Generate TypeScript client
136+
npx openapi-typescript openapi.json -o types.ts
137+
138+
# Generate Python client
139+
openapi-generator-cli generate -i openapi.json -g python
140+
```
141+
142+
### Validation
143+
144+
Validate your spec:
145+
146+
```bash
147+
# Install validator
148+
npm install -g @ibm/openapi-validator
149+
150+
# Validate spec
151+
lint-openapi openapi.json
152+
```
153+
154+
## Command-Line Options
155+
156+
```bash
157+
dspy-cli serve [OPTIONS]
158+
159+
Options:
160+
--save-openapi / --no-save-openapi
161+
Save OpenAPI spec to file on server start
162+
(default: enabled)
163+
--openapi-format [json|yaml] Format for OpenAPI spec file
164+
(default: json)
165+
```
166+
167+
## Examples
168+
169+
### Example: JSON Spec
170+
171+
```bash
172+
cd my-project
173+
dspy-cli serve
174+
```
175+
176+
Creates `openapi.json`:
177+
```json
178+
{
179+
"openapi": "3.1.0",
180+
"info": {
181+
"title": "my-project",
182+
"description": "Automatically generated API for DSPy programs",
183+
"version": "0.1.0"
184+
},
185+
"paths": {
186+
"/MyProgramPredict": {
187+
"post": {
188+
"summary": "Run Program",
189+
"requestBody": {
190+
"content": {
191+
"application/json": {
192+
"schema": {
193+
"$ref": "#/components/schemas/MyProgramPredictRequest"
194+
}
195+
}
196+
}
197+
}
198+
}
199+
}
200+
}
201+
}
202+
```
203+
204+
### Example: YAML Spec
205+
206+
```bash
207+
dspy-cli serve --openapi-format yaml
208+
```
209+
210+
Creates `openapi.yaml`:
211+
```yaml
212+
openapi: 3.1.0
213+
info:
214+
title: my-project
215+
description: Automatically generated API for DSPy programs
216+
version: 0.1.0
217+
paths:
218+
/MyProgramPredict:
219+
post:
220+
summary: Run Program
221+
requestBody:
222+
content:
223+
application/json:
224+
schema:
225+
$ref: '#/components/schemas/MyProgramPredictRequest'
226+
```
227+
228+
## Troubleshooting
229+
230+
### Spec Not Generated
231+
232+
If `openapi.json` isn't created:
233+
1. Check that `--no-save-openapi` wasn't used
234+
2. Verify write permissions in project directory
235+
3. Check server logs for errors
236+
237+
### Missing Program Schemas
238+
239+
If programs don't appear in the spec:
240+
1. Ensure modules subclass `dspy.Module`
241+
2. Verify `forward()` method has type annotations
242+
3. Check that modules are in `src/<package>/modules/`
243+
244+
### Incorrect Metadata
245+
246+
If title/description are wrong:
247+
1. Check `app_id` in `dspy.config.yaml`
248+
2. Verify `description` field in config
249+
3. Ensure config file is in project root
250+
251+
## Implementation Details
252+
253+
- **Utility module**: `src/dspy_cli/utils/openapi.py`
254+
- **Integration point**: `src/dspy_cli/server/app.py` (metadata enhancement)
255+
- **File generation**: `src/dspy_cli/server/runner.py` (on server start)
256+
- **Uses**: FastAPI's built-in `app.openapi()` method

src/dspy_cli/commands/serve.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ def _exec_clean(target_python: Path, args: list[str]) -> NoReturn:
7777
default=True,
7878
help="Enable auto-reload on file changes (default: enabled)",
7979
)
80+
@click.option(
81+
"--save-openapi/--no-save-openapi",
82+
default=True,
83+
help="Save OpenAPI spec to file on server start (default: enabled)",
84+
)
85+
@click.option(
86+
"--openapi-format",
87+
type=click.Choice(["json", "yaml"], case_sensitive=False),
88+
default="json",
89+
help="Format for OpenAPI spec file (default: json)",
90+
)
8091
@click.option(
8192
"--python",
8293
default=None,
@@ -88,7 +99,7 @@ def _exec_clean(target_python: Path, args: list[str]) -> NoReturn:
8899
is_flag=True,
89100
help="Use system Python environment instead of project venv",
90101
)
91-
def serve(port, host, logs_dir, ui, reload, python, system):
102+
def serve(port, host, logs_dir, ui, reload, save_openapi, openapi_format, python, system):
92103
"""Start an HTTP API server that exposes your DSPy programs.
93104
94105
This command:
@@ -103,7 +114,15 @@ def serve(port, host, logs_dir, ui, reload, python, system):
103114
dspy-cli serve --python /path/to/venv/bin/python
104115
"""
105116
if system:
106-
runner_main(port=port, host=host, logs_dir=logs_dir, ui=ui, reload=reload)
117+
runner_main(
118+
port=port,
119+
host=host,
120+
logs_dir=logs_dir,
121+
ui=ui,
122+
reload=reload,
123+
save_openapi=save_openapi,
124+
openapi_format=openapi_format
125+
)
107126
return
108127

109128
target_python = None
@@ -163,7 +182,18 @@ def serve(port, host, logs_dir, ui, reload, python, system):
163182
args.append("--ui")
164183
if reload:
165184
args.append("--reload")
185+
if save_openapi:
186+
args.append("--save-openapi")
187+
args.extend(["--openapi-format", openapi_format])
166188

167189
_exec_clean(target_python, args)
168190
else:
169-
runner_main(port=port, host=host, logs_dir=logs_dir, ui=ui, reload=reload)
191+
runner_main(
192+
port=port,
193+
host=host,
194+
logs_dir=logs_dir,
195+
ui=ui,
196+
reload=reload,
197+
save_openapi=save_openapi,
198+
openapi_format=openapi_format
199+
)

src/dspy_cli/server/app.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from dspy_cli.discovery import discover_modules
1212
from dspy_cli.server.logging import setup_logging
1313
from dspy_cli.server.routes import create_program_routes
14+
from dspy_cli.utils.openapi import enhance_openapi_metadata, create_openapi_extensions
1415

1516
logger = logging.getLogger(__name__)
1617

@@ -117,6 +118,25 @@ async def list_programs():
117118
app.state.modules = modules
118119
app.state.config = config
119120

121+
# Enhance OpenAPI metadata with DSPy-specific information
122+
app_id = config.get("app_id", "DSPy API")
123+
app_description = config.get("description", "Automatically generated API for DSPy programs")
124+
125+
# Create program-to-model mapping
126+
program_models = {module.name: get_program_model(config, module.name) for module in modules}
127+
128+
# Create DSPy extensions
129+
extensions = create_openapi_extensions(config, modules, program_models)
130+
131+
enhance_openapi_metadata(
132+
app,
133+
title=app_id,
134+
description=app_description,
135+
extensions=extensions
136+
)
137+
138+
logger.info("Enhanced OpenAPI metadata with DSPy configuration")
139+
120140
# Register UI routes if enabled
121141
if enable_ui:
122142
from fastapi.staticfiles import StaticFiles

0 commit comments

Comments
 (0)